GraphQL szerver készítése Apollo és Express.js integrációval

A modern webfejlesztésben az adatok hatékony kezelése és kiszolgálása kulcsfontosságú. A REST API-k évtizedekig uralták a terepet, de az utóbbi években egyre nagyobb teret hódít a GraphQL, amely egy rugalmasabb és erősebb alternatívát kínál. Ez a cikk arról szól, hogyan építhetünk egy robusztus GraphQL szervert a népszerű Apollo Server és a bevált Express.js keretrendszer kombinációjával. Ha szeretne olyan API-t fejleszteni, amely pontosan azt adja vissza, amire a kliensnek szüksége van – sem többet, sem kevesebbet –, akkor jó helyen jár.

Miért GraphQL? Miért Apollo és Express?

A GraphQL egy lekérdezési nyelv az API-khoz és egy futásidejű környezet a lekérdezések adatainak kielégítésére. A fő előnye abban rejlik, hogy a kliens specifikálhatja, pontosan milyen adatokat szeretne kapni, elkerülve ezzel az úgynevezett „over-fetching” (túl sok adat lekérése) és „under-fetching” (túl kevés adat lekérése, ami több kérést eredményez) problémákat, amelyek gyakoriak a REST API-k esetében. Egyetlen végponttal, típusbiztos sémával és prediktív adatkérésekkel a GraphQL radikálisan javíthatja az alkalmazások teljesítményét és a fejlesztői élményt.

Az Apollo Server a de facto standard a Node.js alapú GraphQL szerverek építéséhez. Egy átfogó ökoszisztémát biztosít, amely magában foglalja a kliens- és szerveroldali könyvtárakat, valamint eszközöket a fejlesztéshez és a monitorozáshoz. Rendkívül flexibilis és számos funkciót kínál, mint például beépített gyorsítótárazás, előfizetések (subscriptions) támogatása és könnyű integráció a népszerű HTTP keretrendszerekkel.

Az Express.js egy minimalista és rugalmas Node.js webalkalmazás keretrendszer, amely robusztus funkciókészletet biztosít a web- és mobilalkalmazásokhoz. Egyszerűsége és kiterjeszthetősége miatt rendkívül népszerű a fejlesztők körében. Az Express.js és az Apollo Server kombinációja lehetővé teszi, hogy kihasználjuk az Express.js middleware rendszerének erejét (pl. autentikáció, CORS beállítások) miközben élvezzük az Apollo Server GraphQL képességeit.

Előkészületek: A Fejlesztői Környezet Beállítása

Mielőtt belekezdenénk a szerver felépítésébe, győződjünk meg róla, hogy a következő eszközök telepítve vannak a rendszerünkön:

  • Node.js: A szerveroldali JavaScript futtatásához. Ajánlott a legfrissebb LTS (Long Term Support) verzió használata.
  • npm vagy Yarn: Csomagkezelő a Node.js modulok telepítéséhez.

Hozzunk létre egy új projektkönyvtárat és inicializáljuk azt:

mkdir my-graphql-apollo-server
cd my-graphql-apollo-server
npm init -y

Vagy ha Yarn-t használunk:

mkdir my-graphql-apollo-server
cd my-graphql-apollo-server
yarn init -y

GraphQL Alapfogalmak Röviden

Mielőtt rátérnénk a kódra, érdemes áttekinteni néhány alapvető GraphQL fogalmat, amelyek elengedhetetlenek a séma megértéséhez:

  • Séma (Schema): A GraphQL API alapja, amely leírja az összes elérhető adattípust, lekérdezést (queries) és mutációt (mutations). A séma egyfajta „szerződés” a kliens és a szerver között, ami garantálja a típusbiztonságot. A Schema Definition Language (SDL) segítségével írjuk le a sémát.
  • Típusok (Types): A séma építőkövei, amelyek definiálják az adatstruktúrákat. Például egy `User` típusnak lehetnek `id`, `name` és `email` mezői.
  • Lekérdezések (Queries): Adatok olvasására szolgálnak. Hasonlóak a REST API `GET` kéréseihez.
  • Mutációk (Mutations): Adatok írására, módosítására vagy törlésére (CRUD műveletek) szolgálnak. Hasonlóak a REST API `POST`, `PUT`, `PATCH`, `DELETE` kéréseihez.
  • Resolvers (Feloldók): Függvények, amelyek felelősek a séma mezőinek adatainak előállításáért. Amikor egy kliens lekérdezést vagy mutációt küld, a megfelelő resolver fut le, hogy visszaszerezze vagy manipulálja az adatokat.
  • Context (Kontextus): Egy objektum, amelyet az összes resolver megoszt. Hasznos az autentikált felhasználó adatainak, adatbázis kapcsolatoknak vagy más, kérésenkénti állapotoknak a továbbítására.

A Szerver Felépítése Lépésről Lépésre

1. Függőségek Telepítése

Szükségünk lesz az Express.js, GraphQL, Apollo Server, CORS és Body-parser csomagokra. A @apollo/server a legújabb verzió, amely eltér a korábbi apollo-server-express csomagtól.

npm install express graphql @apollo/server cors body-parser
# vagy yarn add express graphql @apollo/server cors body-parser

2. A Séma Definiálása (`schema.js`)

Hozzuk létre a schema.js fájlt, amely tartalmazza a GraphQL séma definícióját SDL nyelven. Definiálunk egy egyszerű User típust, valamint lekérdezéseket a felhasználók listázásához és egy felhasználó azonosító alapján történő lekéréséhez, továbbá egy mutációt új felhasználó létrehozására.

// schema.js
export const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
    updateUser(id: ID!, name: String, email: String): User
    deleteUser(id: ID!): Boolean!
  }
`;

Fontos megjegyezni a #graphql kommentet. Ez egy konvenció, amelyet az Apollo VS Code kiegészítője használ a szintaktikai kiemeléshez. Az ! jel azt jelenti, hogy a mező nem lehet null (kötelező).

3. A Resolverek Implementálása (`resolvers.js`)

Most hozzuk létre a resolvers.js fájlt, amely tartalmazza a séma mezőinek logikáját. Egy egyszerű memóriabeli tömböt fogunk használni az adatok szimulálására.

// resolvers.js
const users = [
  { id: '1', name: 'Alice', email: '[email protected]' },
  { id: '2', name: 'Bob', email: '[email protected]' },
];

export const resolvers = {
  Query: {
    users: () => users,
    user: (parent, { id }) => users.find(user => user.id === id),
  },
  Mutation: {
    createUser: (parent, { name, email }) => {
      const newUser = {
        id: String(users.length + 1),
        name,
        email,
      };
      users.push(newUser);
      return newUser;
    },
    updateUser: (parent, { id, name, email }) => {
      const userIndex = users.findIndex(user => user.id === id);
      if (userIndex === -1) return null;

      const user = users[userIndex];
      if (name) user.name = name;
      if (email) user.email = email;
      
      return user;
    },
    deleteUser: (parent, { id }) => {
      const initialLength = users.length;
      const filteredUsers = users.filter(user => user.id !== id);
      users.length = 0; // Clear the original array
      users.push(...filteredUsers); // Add back the filtered users
      return users.length < initialLength;
    },
  },
};

A resolver függvények paraméterei:

  • parent (vagy root): Az aktuális mező szülőobjektumának eredménye.
  • args: Az aktuális mezőnek átadott argumentumok.
  • context: Az a kontextus objektum, amelyet az Apollo Serverrel konfiguráltunk.
  • info: Az aktuális lekérdezés végrehajtási állapotát tartalmazó információk.

4. Az Apollo Server és Express Integrálása (`index.js`)

Végül hozzuk létre az index.js fájlt, amely összeköti az Express.js-t és az Apollo Servert.

// index.js
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import http from 'http'; // Szükséges lehet később subscriptions-höz

import { typeDefs } from './schema.js';
import { resolvers } from './resolvers.js';

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

async function startApolloServer() {
  const app = express();
  const httpServer = http.createServer(app);

  const server = new ApolloServer({
    typeDefs,
    resolvers,
  });

  // A szerver indítása előtt hívjuk meg a start() metódust
  await server.start();

  // Middleware-ek konfigurálása az Express appon
  app.use(
    '/graphql',
    cors(),
    bodyParser.json(),
    expressMiddleware(server, {
      context: async ({ req }) => {
        // Példa kontextusra: authentikációs token kinyerése
        const token = req.headers.authorization || '';
        // Itt végeznénk az autentikációt, pl. JWT ellenőrzés
        // const user = await getUserFromToken(token);
        return { token /*, user */ };
      },
    }),
  );

  // A szerver indítása
  await new Promise((resolve) => httpServer.listen({ port: PORT }, resolve));
  console.log(`🚀 GraphQL szerver fut itt: http://localhost:${PORT}/graphql`);
  console.log(`Explore at: https://studio.apollographql.com/sandbox/explorer`);
}

startApolloServer();

Ebben a kódban:

  • Importáljuk a szükséges modulokat.
  • Létrehozunk egy Express alkalmazást.
  • Inicializáljuk az ApolloServer-t a typeDefs és resolvers segítségével.
  • Az await server.start() meghívása szükséges az Apollo Server 4-nél az indítás előtt.
  • Az app.use('/graphql', ...) sorban az expressMiddleware integrálja az Apollo Servert az Express útvonalába. Beállítjuk a CORS-t és a Body-parsert a JSON kérések kezeléséhez.
  • A context függvényben gyűjthetjük össze a kérésre specifikus adatokat, például a HTTP fejlécekből származó autentikációs tokent, és elérhetővé tehetjük azt az összes resolver számára.
  • Végül elindítjuk a HTTP szervert a megadott porton.

Ahhoz, hogy az import szintaxis működjön, győződjünk meg róla, hogy a package.json fájlban hozzáadjuk a "type": "module" sort, vagy a fájlok kiterjesztése .mjs. A "type": "module" a javasolt megközelítés:

{
  "name": "my-graphql-apollo-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module", // Ezt a sort adjuk hozzá
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@apollo/server": "^4.x.x",
    "body-parser": "^1.x.x",
    "cors": "^2.x.x",
    "express": "^4.x.x",
    "graphql": "^16.x.x"
  }
}

5. Tesztelés

Indítsuk el a szervert:

npm start
# vagy yarn start

Nyissuk meg a böngészőben a http://localhost:4000/graphql címet. Az Apollo Server automatikusan átirányít minket az Apollo Sandbox-ba, egy interaktív GraphQL IDE-be, ahol könnyedén tesztelhetjük az API-nkat.

Példa lekérdezések (Queries):

Összes felhasználó lekérése:

query GetUsers {
  users {
    id
    name
    email
  }
}

Egy felhasználó lekérése ID alapján:

query GetUserById {
  user(id: "1") {
    id
    name
  }
}

Példa mutációk (Mutations):

Új felhasználó létrehozása:

mutation CreateNewUser {
  createUser(name: "Charlie", email: "[email protected]") {
    id
    name
    email
  }
}

Felhasználó frissítése:

mutation UpdateExistingUser {
  updateUser(id: "1", name: "Alice Smith") {
    id
    name
    email
  }
}

Felhasználó törlése:

mutation DeleteUser {
  deleteUser(id: "2")
}

Haladó Témák és Jó Gyakorlatok

Adatforrások (Data Sources)

Valós alkalmazásokban a resolverek közvetlenül nem kezelik az adatbázis kapcsolatokat. Ehelyett érdemes egy absztrakciós réteget használni, amit az Apollo Server "Data Sources"-nak nevez. Ezek az osztályok becsomagolják az adatgyűjtési logikát (pl. adatbázis lekérdezések, REST API hívások), és segítenek a gyorsítótárazásban és hibakezelésben. Például, ha egy MongoDB adatbázist használunk, lehet egy UsersAPI adatforrásunk, amely a User modellt kezeli.

Autentikáció és Autorizáció

Az Express.js middleware rendszere ideális az autentikáció kezelésére. A kérést feldolgozhatjuk a expressMiddleware előtt, és a felhasználói adatokat elhelyezhetjük a context objektumban. A resolverek ezután hozzáférhetnek ehhez a kontextushoz, és ellenőrizhetik a felhasználó jogosultságait (autorizáció). Például egy JWT token validálása történhet az Express middleware-ben, majd a dekódolt felhasználói objektum a kontextuson keresztül elérhetővé válik minden resolver számára.

Hibakezelés

A GraphQL specifikáció szerint a hibák a válasz errors mezőjében jelennek meg. Az Apollo Server lehetővé teszi egyedi hibaformátumok definiálását és hibák dobását a resolverekből. A GraphQL specifikus hibák (pl. validációs hibák) automatikusan kezelődnek, de az egyedi üzleti logikai hibákhoz érdemes custom error osztályokat létrehozni.

N+1 Probléma és DataLoader

Az N+1 probléma akkor merül fel, amikor egy GraphQL lekérdezés nagyszámú adatbázis lekérdezést indít el. Például, ha lekérdezünk 10 felhasználót, és minden felhasználónak vannak bejegyzései (posts), majd minden egyes felhasználóhoz külön lekérdezéssel hívjuk le a bejegyzéseket, az N+1 (10+1) lekérdezést eredményezhet. A DataLoader egy hatékony segédprogram, amely kötegeléssel (batching) és gyorsítótárazással oldja meg ezt a problémát, jelentősen optimalizálva a teljesítményt.

Előfizetések (Subscriptions)

Az előfizetések lehetővé teszik a kliensek számára, hogy valós idejű frissítéseket kapjanak a szervertől, amikor bizonyos események bekövetkeznek (pl. új chat üzenet, új felhasználó létrehozása). Az Apollo Server támogatja az előfizetéseket WebSocket-ek segítségével, ami kiterjeszti az API lehetőségeit a valós idejű alkalmazások felé.

Környezeti Változók és Konfiguráció

A portszámot, adatbázis kapcsolati stringeket és más érzékeny adatokat soha ne hardkóldjuk a kódba. Használjunk környezeti változókat (pl. .env fájl a dotenv csomaggal), így könnyen konfigurálhatjuk az alkalmazást különböző környezetekben (fejlesztés, tesztelés, éles). A fenti példában a process.env.PORT használata már erre utal.

Összegzés és Következtetés

A GraphQL szerver készítése az Apollo Server és az Express.js integrációjával egy hatékony és modern megközelítés az API-fejlesztéshez. Ez a kombináció a GraphQL rugalmasságát és erősségét ötvözi az Express.js robusztusságával és a Node.js ökoszisztémájának széleskörű eszköztárával.

A sémavezérelt fejlesztés, a pontos adatkérések lehetősége, valamint az Apollo Server által kínált gazdag funkciókészlet mind hozzájárulnak ahhoz, hogy gyorsan és hatékonyan építhessünk skálázható és karbantartható API-kat. A bemutatott lépésekkel egy alapvető, de működőképes GraphQL szervert hoztunk létre, amely stabil alapot biztosít a komplexebb alkalmazások fejlesztéséhez.

Ne habozzon tovább kísérletezni az adatforrásokkal, autentikációval, hibakezeléssel és más haladó funkciókkal. A GraphQL világa tele van lehetőségekkel, és az Apollo/Express stack egy kiváló választás, hogy elmerüljön benne.

Leave a Reply

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