Eine schnelle GraphQL-API mit Fastify und Yoga · Teil 1 von 8 der Serie
Teil 1: Fastify trifft GraphQL Yoga
Richte ein TypeScript-Projekt ohne Build-Schritt ein, setz GraphQL Yoga auf einen Fastify-Server und schick ihm deine ersten Queries.

Quest-Ziele
- Wissen, was du in dieser Serie baust
- TypeScript-Dateien direkt mit Node.js 24 ausführen
- GraphQL Yoga über eine Fastify-Route ausliefern
- Queries mit curl und mit GraphiQL senden
- Eine lesbare Logzeile pro Request bekommen
In dieser Serie baust du die API hinter einem Gildenbrett. Gilden schreiben Quests aus, Abenteurer nehmen sie an und erledigen sie, und jedes Brett, das in einem Browser offen ist, aktualisiert sich in dem Moment, in dem sich etwas ändert. Im letzten Teil bündelt die API ihre Datenbankabfragen, läuft als mehrere Instanzen, cacht ganze Antworten und weist Queries ab, die zu teuer wären.
Mit GraphQL fragt ein Client genau die Felder an, die er braucht, so tief verschachtelt, wie er will, in einem Request an eine Adresse. Das Schema beschreibt alles, was die API kann, mit Typen. Den Preis zahlt der Server: Er muss jede Kombination dieser Felder beantworten, und zwar schnell. Genau darum geht es in dieser Serie.
Die Werkzeuge
Fastify ist ein schneller HTTP-Server für Node.js mit eingebautem strukturiertem Logger.
GraphQL Yoga ist ein GraphQL-Server auf Basis der Fetch-API. Er kann von Haus aus Subscriptions über Server-Sent Events, und seine Plugins decken Caching, Limits und mehr ab.
TypeScript prüft den Code. Node.js führt ihn direkt aus, es gibt also keinen Build-Schritt.
Die Serie
Fastify trifft GraphQL Yoga (dieser Teil)
Eine Datenbank mit PostgreSQL und Drizzle
Das Schema, typisiert von GraphQL Codegen, und das N+1-Problem
Bündeln mit DataLoader
Mutations und Live-Updates mit Subscriptions
Subscriptions über mehrere Instanzen mit Valkey
Ganze Antworten cachen
Query-Limits und Persisted Operations
Jeder Befehl in dieser Serie wurde beim Schreiben ausgeführt, und die Ausgabe, die du siehst, ist die, die er ausgegeben hat.
Was du brauchst
Node.js 24, pnpm und ein Terminal mit curl. Ab dem zweiten Teil brauchst du außerdem Docker, für PostgreSQL und später Valkey. Prüf deine Versionen:
$ node --versionv24.22.2$ pnpm --version12.16.1
Falls pnpm fehlt, installierst du es mit npm, das bei Node.js dabei ist:
$ npm install --global --allow-scripts=pnpm pnpmadded 2 packages in 1s
Das Projekt anlegen
Leg einen Ordner für die API an und lass pnpm eine package.json schreiben:
$ mkdir guild-api$ cd guild-api$ pnpm initWrote to /home/robin/projects/guild-api/package.json{ "name": "guild-api", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devEngines": { "packageManager": { "name": "pnpm", "version": "12.16.1", "onFail": "download" } }, "packageManager": "pnpm@12.16.1", "type": "module"}
"type": "module" ist schon gesetzt, Node.js behandelt also jede Datei als ES-Modul mit import und export. Jetzt kommen die Pakete dazu:
$ pnpm add fastify graphql-yoga graphql @graphql-hive/loggerPackages: +72Progress: resolved 72, reused 0, downloaded 72, added 72, donedependencies:+ @graphql-hive/logger 1.1.1+ fastify 5.12.8+ graphql 17.0.2+ graphql-yoga 6.0.1Done in 414ms using pnpm v12.16.1$ pnpm add -D typescript @types/node@24 pino-prettyPackages: +18Progress: resolved 18, reused 26, downloaded 18, added 18, donedevDependencies:+ @types/node 24.19.2+ pino-pretty 13.2.0+ typescript 7.0.2Done in 401ms using pnpm v12.16.1
graphql ist die Referenzimplementierung von GraphQL, auf der Yoga aufbaut. Yoga 6 loggt über @graphql-hive/logger, und das verbindest du gleich mit dem Logger von Fastify. TypeScript prüft den Code nur, @types/node beschreibt ihm die APIs von Node 24, und pino-pretty macht die Logs beim Entwickeln lesbar.
TypeScript ohne Build-Schritt
Node.js 24 führt .ts-Dateien selbst aus: Es entfernt die Typen und führt aus, was übrig bleibt. Geprüft werden sie dabei nicht, das übernimmt tsc, ohne etwas zu schreiben. Leg die tsconfig.json an:
{
"compilerOptions": {
"module": "nodenext",
"target": "esnext",
"types": ["node"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["src", "scripts", "*.ts"]
}Drei Optionen passen den Code an das Entfernen der Typen in Node an. erasableSyntaxOnly verbietet TypeScript-Features, die echten Code bräuchten, statt einfach entfernt zu werden, etwa enum und Namespaces. verbatimModuleSyntax verlangt import type für Imports, die nur Typen sind, weil Node sie allein nicht unterscheiden kann. Und allowImportingTsExtensions erlaubt dir, ./db.ts mit seinem echten Dateinamen zu importieren, und genau den braucht Node, um die Datei zu finden.
Die Einstellungen kommen in eine .env-Datei, die Node ebenfalls selbst liest, und diese Datei bleibt außerhalb von Git:
PORT=4000node_modules
.envErsetz die scripts in der package.json durch diese:
"scripts": {
"dev": "node --env-file=.env --watch src/server.ts | pino-pretty",
"start": "node --env-file=.env src/server.ts",
"typecheck": "tsc"
},pnpm dev startet den Server neu, sobald sich eine Datei ändert, und macht aus den JSON-Logs lesbare Zeilen. pnpm start ist für die Produktion, mit reinen JSON-Logs, die ein Log-Sammler lesen kann. pnpm typecheck startet tsc.
Der Server
Leg src/server.ts an:
import Fastify, { LogController, type FastifyReply, type FastifyRequest } from 'fastify';
import { createSchema, createYoga } from 'graphql-yoga';
import { Logger } from '@graphql-hive/logger';
import { PinoLogWriter } from '@graphql-hive/logger/writers/pino';
const app = Fastify({
logger: true,
logController: new LogController({ disableRequestLogging: true }),
});
const yoga = createYoga<{ req: FastifyRequest; reply: FastifyReply }>({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`,
resolvers: {
Query: {
hello: () => 'Welcome to the guild board!',
},
},
}),
logging: new Logger({ writers: [new PinoLogWriter(app.log)] }),
});
app.route({
url: yoga.graphqlEndpoint,
method: ['GET', 'POST', 'OPTIONS'],
handler: (req, reply) => yoga.handleNodeRequestAndResponse(req, reply, { req, reply }),
});
await app.listen({ port: Number(process.env.PORT) });Von oben nach unten:
logger: truegibt Fastify seinen Logger pino, der ein JSON-Objekt pro Zeile schreibt. Standardmäßig loggt Fastify zwei Zeilen für jeden Request. DerLogControllerschaltet sie ab, weil Yoga für jeden GraphQL-Request eine bessere schreibt.createYogabekommt ein Schema mit einem Feld,hello, und den Resolver, der es beantwortet. Das Typargument sagt, was jeder Resolver in seinem Kontext findet: den Request und die Reply von Fastify.loggingschickt die Logzeilen von Yoga in den Logger von Fastify, damit sie gleich aussehen und die Request-ID von Fastify tragen.Die Route beantwortet
/graphql, den Endpunkt von Yoga. Sie gibt den Request von Node und die Reply von Fastify an Yoga weiter, das die Antwort selbst schreibt. Das letzte Argument ist der Server-Kontext aus dem Typargument.
Start ihn:
$ pnpm dev[15:17:24.082] INFO (139): Server listening at http://[::1]:4000[15:17:24.083] INFO (139): Server listening at http://127.0.0.1:4000
Die ersten Queries
Schick in einem zweiten Terminal eine Query. GraphQL reist als JSON in einem POST-Request, und eine Query kann auch in der Adresse eines GET-Requests stehen:
$ curl -s localhost:4000/graphql -H 'content-type: application/json' -d '{"query":"{ hello }"}'{"data":{"hello":"Welcome to the guild board!"}}$ curl -s 'localhost:4000/graphql?query=%7Bhello%7D'{"data":{"hello":"Welcome to the guild board!"}}$ curl -s localhost:4000/graphql -H 'content-type: application/json' -d '{"query":"{ goodbye }"}'{"errors":[{"message":"Cannot query field \"goodbye\" on type \"Query\".","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
Die dritte Query fragt nach einem Feld, das es nicht gibt. Yoga prüft jede Operation gegen das Schema, bevor es irgendetwas ausführt, und antwortet mit einem Fehler, der sagt, wo das Problem liegt. Im ersten Terminal steht jetzt eine Zeile pro Request:
[15:17:26.330] INFO (139): Request processed requestId: "req-1" method: "POST" path: "/graphql" operationType: "query" status: 200 errorCount: 0 durationMs: 5[15:17:26.341] INFO (139): Request processed requestId: "req-2" method: "GET" path: "/graphql" operationType: "query" status: 200 errorCount: 0 durationMs: 1[15:17:26.349] INFO (139): Request processed requestId: "req-3" method: "POST" path: "/graphql" operationType: "query" status: 200 errorCount: 1 durationMs: 1
Die Request-ID, ob es eine Query, eine Mutation oder eine Subscription war, wie viele Fehler sie hatte und wie lange sie gedauert hat. Auf durationMs wirst du in dieser Serie oft schauen.
GraphiQL
Öffne localhost:4000/graphql im Browser. Ein Browser fragt nach HTML, also antwortet Yoga mit GraphiQL, einer Spielwiese für deine API. Tipp links { hello } ein und klick auf den Startknopf:
GraphiQL kennt das Schema, es ergänzt also Feldnamen beim Tippen und zeigt ihre Dokumentation. Schneller kannst du eine Query nicht ausprobieren.
Die API antwortet, aber nur mit einem Gruß. Im nächsten Teil gibst du ihr etwas zu erzählen: eine PostgreSQL-Datenbank mit Gilden, Abenteurern und Quests.
