GraphQL API építése a nulláról Node.js és Apollo Server segítségével

Üdvözöllek a modern API fejlesztés világában! Ha valaha is frusztrált voltál a REST API-k korlátaival, a túlzott adatszolgáltatással (over-fetching) vagy az adatok hiányával (under-fetching), akkor a GraphQL megoldást jelenthet a problémáidra. Ebben az átfogó útmutatóban lépésről lépésre végigvezetlek azon, hogyan építs egy robusztus GraphQL API-t a nulláról a népszerű Node.js futtatókörnyezet és az iparági szabványnak számító Apollo Server könyvtár segítségével. Készülj fel, hogy elmélyedj a GraphQL erejében, és megértsd, miért vált ez az adatlekérdezési nyelv az egyik legkeresettebb technológiává!

Miért éppen GraphQL? A REST alternatívája

Mielőtt belevágnánk a kódolásba, tisztázzuk, miért érdemes GraphQL-t használni. A hagyományos REST API-k gyakran előre definiált végpontokkal (endpointokkal) rendelkeznek, amelyek fix adatstruktúrákat szolgáltatnak. Ez azt jelenti, hogy ha egy frontend alkalmazásnak csak néhány mezőre van szüksége egy erőforrásból, akkor is megkapja az összeset (over-fetching). Fordítva, ha több kapcsolódó erőforrásból van szüksége adatokra, akkor több REST hívást kell indítania (under-fetching), ami lassabb teljesítményt és komplexebb kliensoldali logikát eredményez.

A GraphQL ezzel szemben egy olyan adatlekérdezési nyelv, amely a kliensnek adja a kontrollt. A kliens pontosan azt kérdezheti le, amire szüksége van, és csak azt kapja meg, méghozzá egyetlen kérésben. Ez jelentősen optimalizálja a hálózati forgalmat, csökkenti a szerver terhelését, és sokkal rugalmasabbá teszi a frontend fejlesztést. Az alábbiakban a GraphQL fő előnyei:

  • Rugalmasság: A kliens határozza meg, milyen adatokat kapjon vissza.
  • Hatékonyság: Csak a szükséges adatokat kéri le, kevesebb hálózati forgalom.
  • Egyetlen végpont: Nincs szükség több REST végpont kezelésére, minden egyetlen GraphQL végponton keresztül történik.
  • Erőteljes fejlesztői eszközök: A GraphQL Playground (GraphiQL) interaktív felületet biztosít a séma felfedezéséhez és a lekérdezések teszteléséhez.
  • Adatbázis függetlenség: A GraphQL réteg mögött bármilyen adatforrás állhat (relációs adatbázisok, NoSQL adatbázisok, külső API-k).

Előfeltételek

Ahhoz, hogy sikeresen kövesd ezt az útmutatót, az alábbiakra lesz szükséged:

  • Node.js és npm/yarn: Győződj meg róla, hogy a Node.js telepítve van a gépeden (ajánlott az LTS verzió). Az npm (Node Package Manager) automatikusan települ vele együtt.
  • Alapszintű JavaScript/TypeScript ismeret: A példák JavaScriptben lesznek írva, de ha ismered a TypeScriptet, könnyen adaptálhatod őket.
  • Terminál/Parancssor: Kényelmesen tudj parancsokat futtatni.
  • Kódszerkesztő: Például VS Code.

1. lépés: A projekt inicializálása és a szükséges függőségek telepítése

Kezdjük egy új Node.js projekt létrehozásával. Nyisd meg a terminált, és futtasd a következő parancsokat:

mkdir my-graphql-api
cd my-graphql-api
npm init -y

Ez létrehoz egy `package.json` fájlt az alapértelmezett beállításokkal. Most telepítsük a két legfontosabb függőséget: az apollo-server-t és a graphql-t:

npm install apollo-server graphql

Az apollo-server felelős a GraphQL szerver futtatásáért, míg a graphql csomag maga a GraphQL specifikáció implementációja, amelyre az Apollo Server támaszkodik.

Készítsünk egy egyszerű fájlstruktúrát a projektünknek:

my-graphql-api/
├── package.json
├── node_modules/
└── src/
    ├── index.js          # Itt lesz a szerver indítási logikája
    ├── schema.js         # Itt definiáljuk a GraphQL sémánkat
    └── resolvers.js      # Itt implementáljuk a séma logikáját

2. lépés: A GraphQL séma definiálása (Type Definitions)

A GraphQL lényegét a séma (schema) képezi. Ez írja le, milyen típusú adatok érhetők el, és milyen műveleteket lehet végrehajtani rajtuk. A sémát a GraphQL Schema Definition Language (SDL) segítségével definiáljuk. Nyisd meg a `src/schema.js` fájlt, és illessz be a következő kódot:

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

// A "typeDefs" definíálja a GraphQL sémánkat
// Az SDL (Schema Definition Language) segítségével.
const typeDefs = gql`
  # Definiálunk egy "Book" típust a mezőivel.
  type Book {
    id: ID!
    title: String!
    author: String!
    year: Int
  }

  # A "Query" típus definiálja az összes lekérdezhető végpontot.
  type Query {
    books: [Book!]!      # Egy lista könyv lekérdezése
    book(id: ID!): Book  # Egy konkrét könyv lekérdezése ID alapján
  }

  # A "Mutation" típus definiálja az összes írható/módosítható műveletet.
  type Mutation {
    addBook(title: String!, author: String!, year: Int): Book!
    updateBook(id: ID!, title: String, author: String, year: Int): Book
    deleteBook(id: ID!): Boolean
  }
`;

module.exports = typeDefs;

Nézzük meg közelebbről, mit csináltunk:

  • gql: Egy template tag, amelyet az Apollo Server biztosít az SDL stringek feldolgozásához.
  • type Book: Ez egy objektum típus, amely három mezővel rendelkezik: id, title, author és year.
    • ID!, String!, Int: Ezek skalár típusok. A ! jel azt jelenti, hogy a mező kötelező (non-nullable).
    • ID: Egy speciális skalár típus, amely egyedi azonosítókat reprezentál.
    • String, Int: Alapvető típusok stringek és egészek számára. Vannak még Boolean és Float is.
  • type Query: Ez egy speciális objektum típus, amely az összes adatlekérdezési műveletet tartalmazza.
    • books: [Book!]!: Egy mező, amely egy listát ad vissza Book típusú elemekből. A külső ! jelzi, hogy a lista maga nem lehet null, a belső ! pedig azt, hogy a lista elemei sem lehetnek null.
    • book(id: ID!): Book: Egy mező, amely egyetlen Book objektumot ad vissza. Az id: ID! egy argumentum, amit a lekérdezés során át kell adni.
  • type Mutation: Ez egy másik speciális objektum típus, amely az összes adatírási/módosítási műveletet tartalmazza (pl. létrehozás, frissítés, törlés).
    • addBook(title: String!, author: String!, year: Int): Book!: Egy mutáció, amely egy új könyvet hoz létre. Visszatérési értéke az újonnan létrehozott Book.

3. lépés: A Resolvereink írása

A séma definiálja, mit lehet lekérdezni, de nem mondja meg, honnan származzanak az adatok. Erre szolgálnak a resolverek. Egy resolver egy függvény, amely felelős azért, hogy egy adott séma mezőhöz adatokat szolgáltasson. Nyisd meg a `src/resolvers.js` fájlt, és illessz be a következő kódot:

// src/resolvers.js

// Egy egyszerű, memóriában tárolt adatkészlet a példánkhoz.
// Egy éles alkalmazásban ez egy adatbázisból származna.
const books = [
  { id: '1', title: 'The Hitchhiker's Guide to the Galaxy', author: 'Douglas Adams', year: 1979 },
  { id: '2', title: 'The Restaurant at the End of the Universe', author: 'Douglas Adams', year: 1980 },
  { id: '3', title: '1984', author: 'George Orwell', year: 1949 },
];

let nextId = 4; // Az új könyvekhez használt azonosító

const resolvers = {
  Query: {
    books: () => books, // Visszaadja az összes könyvet
    book: (parent, { id }) => books.find(book => book.id === id), // Visszaad egy könyvet ID alapján
  },
  Mutation: {
    addBook: (parent, { title, author, year }) => {
      const newBook = { id: String(nextId++), title, author, year };
      books.push(newBook);
      return newBook;
    },
    updateBook: (parent, { id, title, author, year }) => {
        const bookIndex = books.findIndex(book => book.id === id);
        if (bookIndex === -1) return null; // Könyv nem található

        const book = books[bookIndex];
        if (title) book.title = title;
        if (author) book.author = author;
        if (year) book.year = year;
        return book;
    },
    deleteBook: (parent, { id }) => {
        const bookIndex = books.findIndex(book => book.id === id);
        if (bookIndex === -1) return false;

        books.splice(bookIndex, 1);
        return true;
    },
  },
};

module.exports = resolvers;

A resolver függvények általában négy argumentumot kapnak:

  1. parent (vagy root): Az előző resolver visszatérési értéke a lekérdezés hierarchiájában. Gyakran nem használjuk a legfelső szintű Query/Mutation resolverekben.
  2. args: Azok az argumentumok, amelyeket a GraphQL lekérdezésben vagy mutációban adtunk meg (pl. id, title).
  3. context: Egy objektum, amelyet minden lekérdezéshez hozzáfűzhetünk. Nagyon hasznos autentikációs adatok, adatbázis-kapcsolatok vagy egyéb, a resolverek között megosztandó információk átadására.
  4. info: Egy objektum, amely a lekérdezés végrehajtási állapotával kapcsolatos információkat tartalmaz (pl. a lekérdezett mezők).

Ebben a példában egy egyszerű, memóriában tárolt books tömböt használunk, de egy valós alkalmazásban itt valamilyen adatbázis lekérdezés, egy ORM (Object-Relational Mapper) hívása vagy egy külső API hívása történne.

4. lépés: Az Apollo Server beállítása és indítása

Most, hogy van sémánk és resolverünk, összekapcsolhatjuk őket, és elindíthatjuk az Apollo Servert. Nyisd meg a `src/index.js` fájlt, és illessz be a következő kódot:

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

// Létrehozunk egy ApolloServer példányt,
// átadva a sémát és a resolvert.
const server = new ApolloServer({ typeDefs, resolvers });

// Indítjuk a szervert.
server.listen().then(({ url }) => {
  console.log(`🚀 Szerver készen áll itt: ${url}`);
  console.log(`Explore using GraphQL Playground at ${url}`);
});

Most már csak el kell indítanunk a szervert! Menj vissza a terminálba a projekt gyökérkönyvtárába, és futtasd:

node src/index.js

Ha minden rendben ment, a következő üzenetet kell látnod:

🚀 Szerver készen áll itt: http://localhost:4000/
Explore using GraphQL Playground at http://localhost:4000/

Nyisd meg a böngésződet, és navigálj a http://localhost:4000/ címre. Ekkor meg kell jelennie a GraphQL Playground-nak. Ez egy interaktív eszköz, amely lehetővé teszi a séma felfedezését (Documentation fül), lekérdezések futtatását, mutációk tesztelését, és a válaszok megtekintését. Próbálj ki néhány lekérdezést:

Példa lekérdezés (Query):

query GetBooks {
  books {
    id
    title
    author
    year
  }
}
query GetSingleBook {
  book(id: "1") {
    title
    author
  }
}

Példa mutáció (Mutation):

mutation AddNewBook {
  addBook(title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954) {
    id
    title
    author
    year
  }
}

Futtasd le a mutációt, majd kérdezd le újra a könyveket, hogy lásd az új bejegyzést!

5. lépés: Adatbázis integráció (A valós világban)

Bár a memóriában tárolt tömb egyszerű volt a kezdetekhez, egy valós alkalmazásban adatbázisra lesz szükséged. Az Apollo Server rendkívül rugalmas ezen a téren. A resolverekben egyszerűen kicserélheted a memóriában lévő tömb lekérdezéseket adatbázis műveletekre. Néhány népszerű választás:

  • MongoDB: Mongoose ODM-mel.
  • PostgreSQL/MySQL: Sequelize vagy Prisma ORM-mel.
  • SQLite: Kisebb projektekhez vagy prototípusokhoz.

A context objektum kiválóan alkalmas az adatbázis kapcsolat vagy az ORM példányának átadására a resolverek számára, így elkerülheted a globális változókat és javíthatod a tesztelhetőséget.

// src/index.js (Adatbázis integráció példa - kiegészítés)
// ...
// const { DataBaseConnector } = require('./database'); // Tegyük fel, hogy van egy adatbázis osztályunk
// const db = new DataBaseConnector(); // Inicializáljuk az adatbázis kapcsolatot

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // Itt autentikálhatjuk a felhasználót, vagy átadhatunk adatbázis példányokat
    // const user = await getUserFromToken(req.headers.authorization);
    return {
        // user,
        // db, // Az adatbázis példány elérhető a resolverekben
    };
  },
});
// ...

6. lépés: Autentikáció és Autorizáció

A felhasználók azonosítása és engedélyeik kezelése alapvető egy API-ban. Az autentikációt és autorizációt jellemzően a context objektumon keresztül kezeljük. A context függvényben ellenőrizheted a bejövő kérés fejléceit (pl. JWT token), dekódolhatod azokat, és a felhasználó adatait eltárolhatod a context objektumban. A resolverek ezután hozzáférhetnek a context.user objektumhoz, és eldönthetik, hogy a felhasználó jogosult-e a kért művelet végrehajtására.

// src/resolvers.js (Autentikáció példa a resolverben)
// ...
const resolvers = {
  Query: {
    books: (parent, args, context) => {
      // if (!context.user) throw new AuthenticationError('You must be logged in');
      return books;
    },
    // ...
  },
  // ...
};
// ...

Komplexebb jogosultságkezeléshez érdemes megvizsgálni a GraphQL sémadirektívák (Schema Directives) használatát is.

7. lépés: Hibakezelés

Az Apollo Server jól kezeli a hibákat. Ha egy resolver hibát dob, az Apollo Server automatikusan GraphQL hibaválaszként küldi vissza a kliensnek. Lehetőség van egyéni hibatípusok definiálására is, amelyek részletesebb információkat szolgáltathatnak a kliensnek, miközben elkerülik a belső szerver részletek kiszivárogtatását.

8. lépés: Tesztelés

Ahogy az bármely szoftverfejlesztésnél elengedhetetlen, a GraphQL API-k tesztelése is kulcsfontosságú. A resolvereket unit tesztekkel lehet ellenőrizni, a szerver teljes funkcionalitását pedig integrációs tesztekkel (pl. Jest és Supertest segítségével), amelyek valós GraphQL lekérdezéseket és mutációkat futtatnak a szerver ellen.

Összefoglalás és Következő lépések

Gratulálok! Sikeresen felépítettél egy működő GraphQL API-t a nulláról Node.js és Apollo Server segítségével! Megtanultad, hogyan kell definiálni a sémát, implementálni a resolvereket, és elindítani a szervert. Láttad a GraphQL rugalmasságát és hatékonyságát, és megismerkedtél az alapvető fogalmakkal, amelyek elengedhetetlenek a további fejlesztéshez.

Ez az útmutató csak a kezdet. A GraphQL világa ennél sokkal gazdagabb. Néhány további téma, amit érdemes felfedezned:

  • GraphQL Subscriptions: Valós idejű kommunikáció a kliens és a szerver között (pl. chat alkalmazások).
  • Apollo Client: Egy teljes értékű adatkezelő könyvtár a GraphQL kliensek számára.
  • Adatforrások optimalizálása: N + 1 probléma megoldása DataLoaderrel.
  • Caching: A teljesítmény további javítása.
  • Deployment: A szerver éles környezetbe való telepítése (pl. Heroku, AWS, DigitalOcean).
  • Schema Federation/Stitching: Több GraphQL szerver egyetlen egésszé való egyesítése.

A GraphQL hatalmas potenciállal rendelkezik, és a tudásod ezen a területen rendkívül értékessé tesz a modern webfejlesztésben. Ne habozz kísérletezni, építeni és tovább tanulni! Boldog 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