Was ist GraphQL?
GraphQL ist eine Abfragesprache und Runtime für APIs. Anstatt viele feste Endpunkte bereitzustellen, exponiert ein GraphQL-Server ein einziges, typisiertes Schema. Clients senden Abfragen, mit denen sie exakt die Felder auswählen, die sie benötigen. Der Server löst jedes Feld auf und gibt eine Antwort zurück, die präzise der Struktur der Anfrage entspricht.
Facebook hat GraphQL im Jahr 2012 entwickelt, um ein Problem bei mobilen Anwendungen zu lösen: REST-Endpunkte lieferten entweder zu viele oder zu wenige Daten, und das Abrufen aller für einen Bildschirm benötigten zusammenhängenden Informationen erforderte viele Round-Trips. GraphQL löste beides, indem es dem Client ermöglichte, seine Datenanforderungen in einem einzigen Dokument zu beschreiben.
Das Schema
Das Schema ist der Vertrag. Es definiert die Typen und die verfügbaren Operationen.
# schema.graphql
type User {
id: ID!
name: String!
email: String
posts(first: Int = 10): [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type Query {
user(id: ID!): User
posts(limit: Int): [Post!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}
Das ! markiert ein Feld als non-null. Typen setzen sich zu einem Graphen zusammen, weshalb eine einzige Query von einem User zu dessen Posts und zurück zu den Autoren navigieren kann. Das Schema ist introspektierbar, sodass Tooling automatisch Dokumentationen, Typen und Autocompletion generieren kann.
Queries
Eine Query wählt Felder aus dem Root-Query-Typ aus, wobei Argumente und Variablen verwendet werden können.
# posts.graphql
query RecentPosts($limit: Int!) {
posts(limit: $limit) {
id
title
author {
name
}
}
}
Die Antwort spiegelt die Auswahl exakt wider: keine zusätzlichen Felder, keine fehlenden. Variablen machen Queries wiederverwendbar und ermöglichen es dem Server, die Argumenttypen zu validieren. Fragments extrahieren wiederkehrende Auswahlen zur mehrfachen Verwendung.
# fragment.graphql
fragment PostCard on Post {
id
title
author { name }
}
query Feed {
posts { ...PostCard }
}
Mutationen und Subscriptions
Eine Mutation ändert Daten und gibt eine definierte Struktur zurück – oft einschließlich des aktualisierten Objekts, damit der Client seinen Cache aktualisieren kann.
# create.graphql
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}
Eine Subscription streamt Echtzeit-Updates über eine persistente Verbindung, wie zum Beispiel neue Nachrichten oder Live-Benachrichtigungen. Verwende sie, wenn der Server Daten aktiv an den Client pushen muss; für gelegentliche Aktualisierungen sind Polling oder ein Refetch einfacher.
Resolver
Resolver sind die Schnittstelle zwischen Ihrem Schema und Ihren Daten. Jedes Feld besitzt einen Resolver, der dessen Wert zurückgibt.
// resolvers.js
const resolvers = {
Query: {
user: (_parent, { id }, { db }) => db.users.findById(id),
posts: (_parent, { limit = 10 }, { db }) => db.posts.findMany({ limit }),
},
User: {
posts: (user, { first }, { db }) =>
db.posts.findMany({ where: { authorId: user.id }, limit: first }),
},
Mutation: {
createPost: (_parent, { input }, { db }) => db.posts.create(input),
},
};
Das Context-Objekt enthält gemeinsam genutzte Ressourcen wie die Datenbank und den authentifizierten Benutzer. Resolver sollten klein gehalten werden; Autorisierungsprüfungen gehören hierher oder in die Service-Layer, die sie aufrufen – niemals auf den Client.
Das N+1-Problem
Die häufigste Performance-Falle in GraphQL sind N+1-Queries. Wenn eine Query zehn Posts zurückgibt und der Resolver für den Autor jedes einzelnen Posts die Datenbank abfragt, werden insgesamt elf Queries ausgeführt.
DataLoader löst dieses Problem durch das Batching und Caching von Aufrufen innerhalb eines einzigen Requests.
// loaders.js
import DataLoader from "dataloader";
function createUserLoader(db) {
return new DataLoader(async (ids) => {
const users = await db.users.findByIds(ids);
return ids.map((id) => users.find((u) => u.id === id));
});
}
Der Author-Resolver ruft dann loaders.user.load(post.authorId) auf, und DataLoader sammelt alle Loads innerhalb eines Ticks in einer einzigen Query. Dies ist die wichtigste Optimierung für einen GraphQL-Server.
Clients und Caching
Client-Bibliotheken wie Apollo Client und urql bieten Caching, Normalisierung, Loading- und Error-States sowie eine Integration in UI-Frameworks. Ein normalisierter Cache speichert Entitäten nach Typ und ID, sodass eine Mutation, die einen aktualisierten Post zurückgibt, automatisch jede Query aktualisiert, die darauf referenziert.
Da GraphQL in der Regel einen einzigen POST-Endpunkt verwendet, ist HTTP-Caching weniger effektiv als bei REST. Clients gleichen dies durch normalisierte Caches aus, und Server nutzen Persisted Queries sowie Response-Caching. Gestalte deine Mutations so, dass sie die geänderten Objekte zurückgeben, damit der Client-Cache konsistent bleibt.
GraphQL oder REST?
GraphQL spielt seine Stärken aus, wenn:
- Clients für verschiedene Screens unterschiedliche Felder benötigen.
- Ein Screen Daten aus vielen verwandten Ressourcen benötigt.
- Sie eine einzige, typisierte und selbstdokumentierende API wünschen.
REST ist einfacher, wenn:
- Ressourcen sauber auf Endpunkte abgebildet werden können.
- HTTP-Caching und Status-Codes eine wichtige Rolle spielen.
- Die API klein und stabil ist.
Viele Teams setzen beides ein: GraphQL für die Produkt-API und REST für Webhooks, Datei-Uploads und einfache öffentliche Endpunkte.
Best Practices
- Entwerfen Sie das Schema basierend auf den Anforderungen des Clients, nicht auf den Datenbanktabellen.
- Setzen Sie Non-Null-Typen bewusst ein und versionieren Sie das Schema mithilfe von Deprecations.
- Halten Sie Resolver schlank und lagern Sie die Logik in Services aus.
- Lösen Sie das N+1-Problem von Anfang an mit DataLoader.
- Führen Sie Limits für die Query-Tiefe und Komplexität ein, um Missbrauch zu verhindern.
- Geben Sie bei Mutations die geänderten Objekte zurück, um die Cache-Konsistenz zu gewährleisten.
- Nutzen Sie in der Produktion Persisted Queries, um die Payload-Größe zu reduzieren.
Häufige Fehler
- Das Datenbank-Schema direkt als GraphQL-Schema exponieren.
- Das N+1-Problem zu ignorieren, bis die Performance einbricht.
- In jeder Query alles abzufragen und damit den Hauptvorteil zu verlieren.
- Sich auf vom Client übermittelte Autorisierungen oder Berechtigungen auf Feldebene zu verlassen.
- Tief verschachtelte Queries ohne Depth Limit zu erlauben.
- Davon auszugehen, dass HTTP-Caching genauso funktioniert wie bei REST.
Wie geht es weiter?
GraphQL ist ein leistungsstarker Weg, APIs an den Anforderungen der Clients auszurichten. Setzen Sie es auf dem HTTP-Protokoll auf, bauen Sie den Server mit Node.js und nutzen Sie ihn in React mithilfe einer Client-Library. Modellieren Sie anschließend einen Ihnen bekannten Screen als Schema und Query und erleben Sie, wie natürlich sich die Datenanforderungen abbilden lassen.