Szerveroldali GraphQL implementáció Node.js és Express segítségével

Üdvözöllek a modern API-fejlesztés izgalmas világában! Ha valaha is frusztrált, hogy a REST API-k túl sok vagy túl kevés adatot szolgáltatnak, vagy ha eleged van abból, hogy a frontend és backend csapatok állandóan egyeztetnek az új végpontokról, akkor jó helyen jársz. A GraphQL egy olyan lekérdezőnyelv és futásidejű környezet API-khoz, amely forradalmasítja az adatok kezelésének módját. Ebben a cikkben részletesen bemutatjuk, hogyan valósíthatjuk meg egy szerveroldali GraphQL implementációt Node.js és az Express keretrendszer erejével, lépésről lépésre, példákkal illusztrálva.

Miért pont GraphQL? A REST kihívásai és a GraphQL válaszai

A REST (Representational State Transfer) API-k évtizedekig uralták a webes fejlesztést, és még ma is széles körben használatosak. Egyszerűek, jól érthetőek és könnyen implementálhatók. Azonban ahogy az alkalmazások komplexebbé váltak, és a frontendek igényei egyre specifikusabbá váltak, a REST korlátai is egyre szembetűnőbbé váltak:

  • Over-fetching (túl sok adat lekérése): Gyakran előfordul, hogy egy REST végpont több adatot küld vissza, mint amennyire a kliensnek szüksége van. Ez pazarlás, lassítja a hálózati forgalmat és a kliens oldali feldolgozást.
  • Under-fetching (túl kevés adat lekérése): Előfordulhat, hogy egyetlen végpont nem adja vissza az összes szükséges adatot, ami miatt a kliensnek több kérést kell indítania különböző végpontokra (az ún. N+1 probléma), ami szintén lassítja az alkalmazást.
  • Merev verziózás: A REST API-k verziózása (pl. /v1/, /v2/) bonyolult lehet, és gyakran megnehezíti az API folyamatos fejlesztését anélkül, hogy megtörné a régebbi klienseket.
  • Több végpont kezelése: Egy összetett alkalmazásban rengeteg különböző végpontra lehet szükség, ami nehezen karbantarthatóvá teszi az API-t.

A GraphQL ezekre a problémákra kínál elegáns megoldást. Lényege, hogy a kliens pontosan azt kéri le, amire szüksége van, és nem többet. Egyetlen végponttal dolgozunk, és a kliens dönti el, milyen adatokat és milyen struktúrában szeretne megkapni. Ez hatalmas rugalmasságot ad a frontend fejlesztőknek, és drámaian javítja az adatlekérések hatékonyságát.

Miért Node.js és Express a GraphQL-hez?

A Node.js egy JavaScript futásidejű környezet, amely lehetővé teszi a szerveroldali JavaScript fejlesztést. Az Express pedig a Node.js legnépszerűbb és leginkább elterjedt webes keretrendszere. Ez a kombináció több szempontból is ideális a GraphQL szerverek építéséhez:

  • Egységes nyelv: Ha a frontend is JavaScriptben íródott (pl. React, Vue, Angular), akkor a backend is JavaScriptben történő fejlesztése jelentősen egyszerűsíti a teljes stack kezelését, és lehetővé teszi a kódmegosztást.
  • Aszinkron, eseményvezérelt architektúra: A Node.js nem-blokkoló I/O modellje kiválóan alkalmas az API-khoz, amelyek gyakran sok párhuzamos kérést szolgálnak ki adatbázisokból vagy más mikroszolgáltatásokból.
  • Nagy közösség és ökoszisztéma: Rengeteg modul, könyvtár és eszköz áll rendelkezésre, amelyek megkönnyítik a fejlesztést és a hibakeresést.
  • Teljesítmény: Jól optimalizált aszinkron műveletekkel a Node.js rendkívül gyors és skálázható lehet.

A GraphQL alapjai: Séma és Resolverek

Mielőtt belevágnánk az implementációba, értsük meg a GraphQL két sarokkövét: a sémát és a resolvert.

1. GraphQL Séma (Schema Definition Language – SDL)

A séma a GraphQL API szerződése a kliensek felé. Meghatározza, milyen adatok kérdezhetők le, milyen műveletek hajthatók végre (létrehozás, frissítés, törlés), és milyen típusokkal dolgozunk. A séma a GraphQL Schema Definition Language (SDL) segítségével íródik. Nézzünk meg néhány alapvető elemet:

  • Típusok (Types): A séma alapvető építőkövei. Meghatározzák az objektumok szerkezetét.
    • Skaláris típusok: Alapvető adattípusok, mint String, Int, Float, Boolean, ID.
    • Objektum típusok: Egyedi, komplex típusok, amelyek skaláris és más objektum típusokból állnak. Például egy Book (könyv) típusnak lehet title: String és author: Author mezője.
  • Query típus: Ez egy speciális objektum típus, amely az API összes adatlekérdező műveletét (olvasás) definiálja. Ha adatot akarunk lekérdezni, azt mindig a Query típuson keresztül tesszük.
  • Mutation típus: Szintén egy speciális objektum típus, amely az API összes adatmódosító műveletét (létrehozás, frissítés, törlés) definiálja.
  • Input típusok: Komplex bemeneti adatok átadására szolgálnak a mutációknak.
  • ! (non-nullable): A mező típusa után elhelyezve azt jelenti, hogy az adott mező nem lehet null.
  • [] (lista): A típus után elhelyezve azt jelenti, hogy a mező az adott típusú elemek listája.

2. Resolverek (Resolvers)

A resolverek azok a függvények, amelyek ténylegesen lekérik vagy módosítják az adatokat az adatbázisból, egy másik API-ból vagy bármely más adatforrásból. Minden egyes mezőhöz a sémában tartozhat egy resolver függvény. Amikor egy kliens GraphQL lekérdezést küld, a GraphQL szerver bejárja a sémát, és meghívja a megfelelő resolver függvényeket az adatok begyűjtésére.

Implementáció Node.js és Express segítségével: Lépésről lépésre

1. Projekt inicializálása és függőségek telepítése

Először is hozzunk létre egy új Node.js projektet, és telepítsük a szükséges csomagokat. A GraphQL implementációhoz az apollo-server-express könyvtárat fogjuk használni, amely az Express-szel való integrációt teszi rendkívül egyszerűvé.


mkdir graphql-szerver
cd graphql-szerver
npm init -y
npm install express graphql apollo-server-express

Ezek a csomagok:

  • express: A webes keretrendszerünk.
  • graphql: A GraphQL specifikáció implementációja.
  • apollo-server-express: Az Apollo Server, amely egy robusztus, production-ready GraphQL szerver, Express integrációval.

2. A GraphQL séma definiálása

Hozzuk létre a src/schema.js fájlt, és definiáljuk benne egy egyszerű könyvtár API sémáját, amely könyveket és szerzőket kezel.


// src/schema.js
const { gql } = require('apollo-server-express');

// Mock adatbázisunk
const books = [
  {
    id: '1',
    title: 'Az elveszett kód',
    authorId: '1',
  },
  {
    id: '2',
    title: 'A digitális erőd',
    authorId: '1',
  },
  {
    id: '3',
    title: 'Angyalok és démonok',
    authorId: '2',
  },
];

const authors = [
  {
    id: '1',
    name: 'Dan Brown',
  },
  {
    id: '2',
    name: 'J.K. Rowling',
  },
];

// GraphQL Schema Definition Language (SDL)
const typeDefs = gql`
  # Skaláris típusok: String, Int, Float, Boolean, ID

  # Objektum típus a könyveknek
  type Book {
    id: ID!
    title: String!
    author: Author!
  }

  # Objektum típus a szerzőknek
  type Author {
    id: ID!
    name: String!
    books: [Book!]! # Egy szerzőnek több könyve is lehet
  }

  # Query típus: Itt definiáljuk az adatlekérdezéseket
  type Query {
    books: [Book!]! # Összes könyv lekérdezése
    book(id: ID!): Book # Egy adott könyv lekérdezése ID alapján
    authors: [Author!]! # Összes szerző lekérdezése
    author(id: ID!): Author # Egy adott szerző lekérdezése ID alapján
  }

  # Input típus mutációkhoz: Új könyv hozzáadásához
  input AddBookInput {
    title: String!
    authorId: ID!
  }

  # Mutation típus: Itt definiáljuk az adatmanipuláló műveleteket
  type Mutation {
    addBook(input: AddBookInput!): Book! # Új könyv hozzáadása
    addAuthor(name: String!): Author! # Új szerző hozzáadása
  }
`;

module.exports = { typeDefs, books, authors };

Nézzük meg röviden a séma főbb részeit:

  • Book és Author: Objektum típusok a könyvek és szerzők reprezentálására. A ! jelzi a kötelező mezőket.
  • Query: Két lekérdezést definiálunk: books (az összes könyv lekérdezésére) és book(id: ID!) (egy adott ID-jú könyv lekérdezésére). Hasonlóan a szerzőkre is.
  • AddBookInput: Egy bemeneti típus, amelyet a addBook mutáció használ a komplex bemeneti adatok kezelésére.
  • Mutation: Két mutációt definiálunk: addBook (új könyv hozzáadására) és addAuthor (új szerző hozzáadására).

3. A resolverek implementálása

Most pedig hozzuk létre a src/resolvers.js fájlt, és írjuk meg a resolver függvényeket, amelyek „életre keltik” a sémánkat. Ezek a függvények felelnek azért, hogy az adatokat a mock adatbázisunkból (vagy valós adatbázisból) lekérjék, és visszaadják a kliensnek.


// src/resolvers.js
const { books, authors } = require('./schema');

// Segédfüggvény a következő ID generálásához
const generateId = (array) => {
    return (parseInt(array[array.length - 1].id) + 1).toString();
};

const resolvers = {
  Query: {
    books: () => books, // Visszaadja az összes könyvet
    book: (parent, args) => books.find(book => book.id === args.id), // Keresés ID alapján
    authors: () => authors, // Visszaadja az összes szerzőt
    author: (parent, args) => authors.find(author => author.id === args.id), // Keresés ID alapján
  },
  Mutation: {
    addBook: (parent, args) => {
      const { title, authorId } = args.input;
      const newBook = {
        id: generateId(books), // Generálunk egy új ID-t
        title,
        authorId,
      };
      books.push(newBook); // Hozzáadjuk a mock adatbázishoz
      return newBook;
    },
    addAuthor: (parent, args) => {
        const { name } = args;
        const newAuthor = {
            id: generateId(authors),
            name,
        };
        authors.push(newAuthor);
        return newAuthor;
    },
  },
  // Kapcsolódó resolverek
  Book: {
    author: (parent) => authors.find(author => author.id === parent.authorId), // Egy könyvhöz tartozó szerző lekérése
  },
  Author: {
    books: (parent) => books.filter(book => book.authorId === parent.id), // Egy szerzőhöz tartozó könyvek lekérése
  },
};

module.exports = resolvers;

Fontos megfigyelések a resolverekkel kapcsolatban:

  • Query resolverek: A books és authors resolverek egyszerűen visszaadják a teljes listát. A book és author resolverek a args objektumból veszik ki az id paramétert, és annak alapján keresik meg a megfelelő elemet.
  • Mutation resolverek: Az addBook és addAuthor resolverek a args objektumból olvassák ki a bemeneti adatokat, létrehoznak egy új objektumot, hozzáadják azt a mock adatbázishoz, és visszaadják az újonnan létrehozott elemet.
  • Kapcsolódó resolverek (Book és Author): Ezek a resolverek felelősek a komplex típusok mezőinek feloldásáért. Például a Book típus author mezője esetén a resolver megkapja a „parent” (szülő) objektumot (ami ebben az esetben a Book objektum), és annak authorId mezőjét felhasználva kikeresi a megfelelő szerzőt az authors tömbből. Ez teszi lehetővé, hogy a kliens a könyv lekérdezésekor egyben a szerzőjét is lekérdezhesse anélkül, hogy külön kérést indítana.

4. Az Express szerver beállítása az Apollo Serverrel

Végül, hozzuk létre az index.js fájlt, amelyben inicializáljuk az Express alkalmazást, integráljuk az Apollo Servert, és elindítjuk a szervert.


// index.js
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { typeDefs } = require('./src/schema');
const resolvers = require('./src/resolvers');

async function startApolloServer() {
  const app = express();
  const server = new ApolloServer({
    typeDefs,
    resolvers,
    // context: ({ req }) => ({ token: req.headers.token }) // Példa a context használatára autentikációhoz
  });

  await server.start(); // Az Apollo Server indítása

  // Az Apollo Server middleware csatlakoztatása az Expresshez
  server.applyMiddleware({ app, path: '/graphql' });

  const PORT = process.env.PORT || 4000;

  app.listen({ port: PORT }, () =>
    console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`)
  );
}

startApolloServer();

Ebben a fájlban:

  • Létrehozunk egy Express alkalmazást.
  • Példányosítjuk az ApolloServert, átadva neki a typeDefs (séma) és resolvers (resolverek) objektumokat.
  • Az await server.start() metódus elindítja az Apollo Servert.
  • A server.applyMiddleware({ app, path: '/graphql' }) összekapcsolja az Apollo Servert az Express alkalmazásunkkal a /graphql útvonalon. Ez az útvonal lesz az egyetlen végpont, amellyel a kliensek kommunikálnak.
  • Végül elindítjuk az Express szervert a 4000-es porton (vagy a környezeti változóban megadott porton).

5. A szerver futtatása és tesztelése

Most már elindíthatjuk a szerverünket:


node index.js

A konzolon látni fogjuk a következő üzenetet: 🚀 Server ready at http://localhost:4000/graphql. Nyissuk meg ezt az URL-t a böngészőnkben. Az Apollo Server beépített GraphQL Playground felületet biztosít, ahol könnyedén tesztelhetjük az API-nkat.

Próbáljunk meg egy lekérdezést futtatni:


query GetBooksAndAuthors {
  books {
    id
    title
    author {
      name
    }
  }
  authors {
    id
    name
    books {
      title
    }
  }
}

És egy mutációt:


mutation AddNewBook {
  addBook(input: { title: "A programozó utikalauza", authorId: "1" }) {
    id
    title
    author {
      name
    }
  }
}

Láthatjuk, hogy az eredmény pontosan azt az adatstruktúrát adja vissza, amit kértünk. Ez a GraphQL ereje!

Fejlettebb témák és megfontolások

Bár az alapok most már megvannak, a valós alkalmazásokban számos további szempontot figyelembe kell venni:

  • Adatbázis-integráció: A resolverekben a mock adatbázis helyett valós adatbázis (pl. MongoDB Mongoose-szal, PostgreSQL Sequelize-vel vagy Prisma-val) lekérdezési logikája szerepelne.
  • Hitelesítés és jogosultságkezelés (Authentication & Authorization): A context objektum kiválóan alkalmas a felhasználói tokenek vagy egyéb hitelesítési információk tárolására, amelyeket aztán a resolverekben felhasználhatunk a jogosultság ellenőrzésére. Middleware-eket is használhatunk az Express szintjén.
  • Hibakezelés: A GraphQL lehetővé teszi a specifikus hibaüzenetek és -kódok visszaadását, ami segíti a kliensoldali hibakezelést. Egyéni hibaobjektumokat is definiálhatunk.
  • Adatbetöltők (Data Loaders): Az N+1 probléma elkerülésére (amikor egy lista minden eleméhez külön adatbázis-lekérdezés indul) a Data Loader egy rendkívül hasznos eszköz. Ez kötegelten hajtja végre a lekérdezéseket, jelentősen javítva a teljesítményt.
  • Előfizetések (Subscriptions): A GraphQL képes valós idejű adatok streamelésére is WebSocketek segítségével, lehetővé téve a kliensek számára, hogy értesítéseket kapjanak az adatok változásairól (pl. chat alkalmazásokban).
  • Gyorsítótárazás (Caching): A GraphQL lekérdezések gyorsítótárazása komplexebb lehet, mint a REST esetében, mivel a lekérdezések dinamikusak. Kliensoldali gyorsítótárak (pl. Apollo Client Cache) és szerveroldali megoldások is léteznek.

Összegzés és jövőbeli kilátások

A Node.js és Express segítségével felépített GraphQL szerveroldali implementáció hatalmas rugalmasságot és hatékonyságot kínál a modern API-fejlesztésben. Lehetővé teszi a kliensek számára, hogy pontosan azt az adatot kapják meg, amire szükségük van, csökkentve az over-fetching és under-fetching problémákat, és felgyorsítva az API evolúcióját.

Bár a kezdeti tanulási görbe létezik, a GraphQL előnyei – mint a jobb fejlesztői élmény, a kliensoldali rugalmasság, a robusztus típusrendszer és az erős tooling – messze felülmúlják ezt a kezdeti befektetést. Ha egy hatékony, skálázható és karbantartható API-ra vágysz, érdemes megfontolnod a GraphQL bevezetését a következő projektjeidbe. A Node.js és az Express pedig tökéletes partnerek ehhez a küldetéshez.

Remélem, ez a részletes útmutató segített megérteni a GraphQL szerveroldali implementációjának alapjait és gyakorlati lépéseit. Jó kódolást!

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük