GraphQL-Überblick: Vom Schema zur föderierten API
Ein Durchgang durch GraphQL – vom typisierten Schema über Abfragen und Resolver bis zu Echtzeit, Betrieb und föderierter Architektur, eingeordnet am Stand von Oktober 2024.
Ein typisiertes Schema und mindestens ein Query-Einstieg reichen, um sehr unterschiedliche Fragen an eine GraphQL-API zu stellen. Häufig laufen diese Operationen über einen HTTP-Endpunkt; die GraphQL-Spezifikation selbst schreibt den Transport aber nicht vor. Die entscheidende Frage dahinter lautet, wer die Form der Antwort bestimmt. Bei einer klassischen REST-Schnittstelle definiert der Server die Repräsentation einer Route. GraphQL lässt den Client daraus die benötigten Felder auswählen. Die Kontrolle bleibt beim Backend, weil jede erlaubte Auswahl vorher im Schema festgeschrieben ist.
Dieser Beitrag geht den Weg vom Schema bis zur verteilten Architektur einmal ganz durch, damit man die Stationen kennt, bevor man sich in einer davon verliert. Zu einzelnen Stationen – wie genau die Ausführung im Inneren abläuft – schreibe ich eigene, tiefere Beiträge, etwa GraphQL-Konzepte: Schema, Resolver, Ausführung. Hier geht es um das Ganze, mit dem Werkzeugstand von Oktober 2024: Apollo Server 4 dient als Node.js-Beispiel, und der Federation-Abschnitt bezieht sich auf Apollo Federation 2. GraphQL-over-HTTP war damals und ist auch im Juli 2026 noch eine Stage-2-Draft-Spezifikation; sie beschreibt die HTTP-Semantik für Queries und Mutations, nicht den Transport von Subscriptions.
GraphQL verbindet mehrere Verantwortungen, ohne sie zu einem einzigen Baustein zu machen.
- Client-Operation · beschreibt die benötigte Form der Antwort
- Schema · begrenzt Typen, Felder und zulässige Operationen
- Ausführung · prüft und plant die angeforderte Auswahl
- Resolver · löst genau ein Feld innerhalb seines Kontexts auf
- Datenquelle · besitzt Daten und fachliche Regeln hinter dem Resolver
- Gateway · kann mehrere Subgraphen unter einem Vertrag koordinieren
Das Schema bleibt dabei Vertrag und Orientierungspunkt. Es ersetzt weder Fachlogik noch Datenbesitz der angeschlossenen Systeme.
GraphQL ist eine Spezifikation, kein Framework
Der häufigste Irrtum am Anfang: GraphQL sei eine Bibliothek, die man installiert. Ist es nicht. GraphQL ist eine Spezifikation – sie beschreibt eine Abfragesprache und wie ein Server eine solche Abfrage auszuwerten hat. Es gibt Referenzimplementierungen wie graphql-js, und darauf setzen Server wie Apollo Server oder GraphQL Yoga auf, aber das Herzstück ist ein Vertrag, keine Laufzeitumgebung.
Aus dieser einen Tatsache folgt fast alles Weitere. Weil der Vertrag typisiert ist, kann der Client vor dem ersten Aufruf wissen, welche Felder es gibt und welchen Typ sie haben. Und weil der Server die Auswahl auswertet statt nur eine festgelegte Repräsentation auszuliefern, kann ein gemeinsamer HTTP-Endpunkt viele verschiedene Fragen beantworten. Ein einzelner Endpunkt ist die verbreitete Bereitstellung, keine Forderung der transportagnostischen Spezifikation.
flowchart LR
Client["Client<br/>formuliert die Frage"] -->|eine Query| Endpoint["ein GraphQL-Endpunkt"]
Endpoint --> Engine["Ausführung<br/>gegen das Schema"]
Engine --> DB[("Datenbank")]
Engine --> REST["REST-API"]
Engine --> Service["anderer Dienst"]
Engine -->|genau die angefragten Felder| Client
Wichtig dabei: Hinter dem einen Endpunkt kann alles Mögliche liegen. GraphQL ist keine Datenbank und ersetzt keine – es ist eine Schicht, die verschiedene Quellen unter einem gemeinsamen, typisierten Modell zusammenführt.
Das Schema zuerst
Alles beginnt beim Schema. Es wird in der Schema Definition Language geschrieben, kurz SDL, und beschreibt die Typen und ihre Beziehungen. Ein kleines Beispiel aus der Welt eines Blogs:
type Author {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: Author!
}
type Query {
author(id: ID!): Author
posts: [Post!]!
}
Drei Dinge lohnen den genauen Blick. Erstens die Modifizierer: Das Ausrufezeichen bedeutet „darf nicht null sein“, die eckigen Klammern bedeuten „Liste“. [Post!]! ist also eine nicht-nullbare Liste von nicht-nullbaren Posts – eine sehr präzise Aussage, die ein Client sich merken kann. Zweitens die Beziehungen: Author verweist auf Post und umgekehrt. Das Schema beschreibt keinen flachen Katalog, sondern einen Graphen aus Knoten und Kanten – daher der Name.
Drittens, und das ist der Kern: Query ist ein besonderer Typ. Optional kommen Mutation fürs Schreiben und Subscription für laufende Ergebnisse hinzu. Diese Operationstypen sind die Einstiegspunkte in den Graphen. Was dort nicht als Feld steht, kann kein Client auswählen. Damit ist das Schema ein echter Vertrag: Es sagt verbindlich, welche Operationen und Antwortformen erlaubt sind.
Zwei Werkzeuge machen diesen Vertrag im Alltag deutlich leichter zu handhaben. Felder lassen sich mit @deprecated(reason: "…") als veraltet markieren, ohne sie sofort zu entfernen – Clients bekommen den Hinweis, brechen aber nicht. Und für Mutationen gibt es eigene Input-Typen, damit Eingaben nicht mit Ausgabetypen verwechselt werden. Beides sind kleine Zeichen dafür, dass GraphQL für langlebige APIs gedacht ist, nicht für den Wegwerf-Endpunkt.
Die Abfrageseite: der Client fragt genau
Eine Abfrage sieht dem Datenteil der Antwort ähnlich, den sie erzeugt. Man beschreibt die gewünschte Auswahl, und die erfolgreiche Antwort folgt dieser Form:
query AuthorWithPosts($id: ID!) {
author(id: $id) {
name
posts {
title
}
}
}
Der Client fragt hier nach dem Namen eines Autors und den Titeln seiner Posts – und bekommt nur das. Kein überflüssiges Feld, keine zweite Runde für die Posts. Das $id ist eine Variable; Abfragen werden nicht durch String-Bastelei parametrisiert, sondern über typisierte Variablen, die der Server prüft.
Zwei weitere Bausteine tauchen schnell auf. Fragmente bündeln wiederkehrende Feldmengen, damit man sie nicht in jeder Query wiederholt. Und Introspection erlaubt es, das Schema selbst abzufragen – der Server beschreibt sich, weshalb Werkzeuge wie GraphiQL Autovervollständigung und Dokumentation ohne zusätzliche Konfiguration anbieten können. Der Vertrag ist also nicht nur verbindlich, er ist auch auslesbar.
Die Ausführung: Resolver für Resolver
Wie wird aus der Abfrage eine Antwort? Der Server durchläuft die Abfrage von oben nach unten und ruft für jedes Feld eine Funktion auf, den Resolver. Jeder Resolver bekommt vier Argumente in fester Reihenfolge: das Elternobjekt (source), die Argumente des Feldes (args), einen anfragebezogenen Kontext (context) und Informationen über den Ausführungszustand (info).
const resolvers = {
Query: {
author: (source, { id }, context) => context.db.loadAuthor(id),
},
Author: {
posts: (author, args, context) => context.db.loadPostsForAuthor(author.id),
},
};
Man sieht das Prinzip: Der Resolver für author liefert einen Autor, und dessen Rückgabewert wird zum source für die Feld-Resolver darunter – der Resolver für posts bekommt den Autor als erstes Argument und lädt darüber seine Posts. So wandert die Ausführung als Kette durch den Graphen. Ein Resolver kann synchron, per Promise oder async arbeiten und dabei jede Datenquelle ansprechen – eine Datenbank, eine REST-API, einen anderen Dienst.
Genau hier lauert die bekannteste Falle, das N+1-Problem. Fragt jemand zehn Autoren mit ihren Posts ab, läuft der posts-Resolver zehnmal und macht zehn einzelne Datenbankabfragen – plus die eine für die Autoren. Die Lösung heißt Batching: Man sammelt die zehn Anfragen innerhalb eines Ausführungstakts ein und beantwortet sie mit einer einzigen Abfrage. Die dafür übliche Bibliothek ist DataLoader.
flowchart TB
subgraph Ohne["Ohne DataLoader (N+1)"]
Q1["10 Autoren"] --> R1["10× posts-Resolver"]
R1 --> DB1[("10 einzelne Abfragen")]
end
subgraph Mit["Mit DataLoader"]
Q2["10 Autoren"] --> R2["10× posts-Resolver"]
R2 --> L["DataLoader sammelt IDs"]
L --> DB2[("1 gebündelte Abfrage")]
end
Wie die Ausführung im Detail durch den Baum läuft, in welcher Reihenfolge Geschwisterfelder ausgewertet werden und warum Mutationen anders behandelt werden als Abfragen – das ist ein Thema für sich, dem ich im Konzepte-Beitrag einen eigenen, tieferen Abschnitt widme. Für diesen Überblick reicht: Eine Abfrage ist ein Pfad durch den Graphen, und jeder Knoten auf dem Pfad wird von einem Resolver bedient.
Schreiben und Echtzeit
Lesen ist nur die halbe Geschichte. Mutationen sind der schreibende Teil und sehen aus wie Abfragen, tragen aber eine Absicht:
mutation CreatePost($input: PostInput!) {
createPost(input: $input) {
id
title
}
}
Der Unterschied ist mehr als kosmetisch: Auf oberster Ebene führt der Server Mutationen nacheinander aus, nicht parallel, damit sich zwei Schreibvorgänge nicht ins Gehege kommen. Und weil eine Mutation ein Ergebnis zurückgibt – hier den angelegten Post –, kann der Client seinen Zustand direkt aus der Antwort aktualisieren, ohne neu zu laden.
Für laufende Aktualisierungen gibt es Subscriptions. Der Client abonniert ein Ereignis, und der Server schickt neue Daten, sobald sie entstehen. Klassisch lief das über WebSockets; inzwischen ist auch der Transport über Server-Sent Events verbreitet, gerade wenn eine einfache, einseitige Verbindung genügt – dazu habe ich an anderer Stelle mehr geschrieben. Der GraphQL-over-HTTP-Entwurf formuliert die HTTP-Semantik für Queries und Mutations aus, sodass Server und Clients verschiedener Hersteller verlässlicher zusammenspielen. Subscriptions liegen ausdrücklich außerhalb seines Geltungsbereichs.
Betrieb: Fehler, Paginierung, Auth, Caching
Zwischen dem ersten funktionierenden Schema und einer API, die in Produktion trägt, liegen ein paar Themen, die man nicht überspringen sollte.
Fehler behandelt GraphQL eigen: Eine Antwort kann gleichzeitig Daten und ein errors-Feld enthalten. Ein fehlgeschlagenes Teilfeld macht nicht die ganze Antwort kaputt – der Rest kommt trotzdem. Das ist mächtig, verlangt aber Disziplin, damit Clients erwartbare Fehler von echten Pannen unterscheiden können.
Paginierung löst man selten über simple offset/limit-Argumente, sondern meist über cursorbasierte Verbindungen – das Connections-Muster mit Kanten und Seiteninformationen, das aus der Relay-Welt stammt und sich als Konvention durchgesetzt hat. Es ist etwas umständlicher zu lesen, aber stabil, wenn sich die Liste unter den Fingern ändert.
Authentifizierung und Autorisierung sind nicht dasselbe. Die Identität wird häufig beim Aufbau des context ermittelt und von dort an die Ausführung weitergegeben. Die eigentlichen Zugriffsregeln können in Resolvern, Direktiven, der Domänenschicht oder der Datenzugriffsschicht liegen. Entscheidend ist, dass jede relevante Zugriffsstelle dieselbe Policy durchsetzt und nicht bloß darauf vertraut, dass ein Feld im Schema verborgen bleibt.
Caching schließlich ist anders als bei vielen REST-APIs, bei denen URL und HTTP-Methode einen naheliegenden Cache-Schlüssel bilden. GraphQL über HTTP kann ebenfalls HTTP-Caching nutzen, doch unterschiedliche Operationen teilen häufig dieselbe Adresse und liegen oft im Request-Body. Clients wie Apollo Client ergänzen deshalb einen normalisierten Objekt-Cache: Objekte mit stabiler Identität lassen sich über mehrere Abfragen hinweg gemeinsam aktualisieren.
Architektur: eine Schicht, die aggregiert
Damit sind wir bei der eigentlichen Stärke von GraphQL im Großen. Weil hinter dem Endpunkt beliebige Quellen liegen können, eignet sich GraphQL hervorragend als Aggregationsschicht – ein Backend for Frontend, das mehrere Dienste, Datenbanken und fremde APIs unter einem Modell zusammenzieht, zugeschnitten auf das, was die Oberfläche wirklich braucht.
flowchart TB
App["Web- & Mobile-Client"] --> GW["GraphQL-Gateway<br/>(föderiertes Schema)"]
GW --> S1["Subgraph: Nutzer"]
GW --> S2["Subgraph: Bestellungen"]
GW --> S3["Subgraph: Katalog"]
S1 --> DB1[("Nutzer-DB")]
S2 --> DB2[("Bestell-DB")]
S3 --> REST["Katalog-REST"]
Wird das Schema zu groß für ein einzelnes Team, kommt Federation ins Spiel. Statt eines Monolithen betreibt jedes Team einen eigenen Teilgraphen (Subgraph), und ein Gateway setzt daraus für den Client ein einziges, zusammenhängendes Schema zusammen. Der Client merkt davon nichts – er sieht weiterhin einen Graphen. Federation ist heute der übliche Weg, wenn GraphQL organisatorisch skalieren muss; die ältere Schema-Stitching-Variante trifft man noch in Bestandssystemen.
Bei der Wahl des Servers hat sich das Feld sortiert. Apollo Server 4 ist die sichere Standardwahl für einen gewöhnlichen Node.js-Stack – das größte Ökosystem, gute Federation-Unterstützung, breite Verwendung in der Praxis. GraphQL Yoga ist die schlankere Alternative, besonders wenn man auf Edge-Runtimes deployt, wo Apollo Server zu schwergewichtig wäre. Beide setzen auf graphql-js in Version 16 als gemeinsamem Fundament.
Das Repository als roter Faden
Wer das alles anfassen will, findet in introduction-graphql die Stationen als einzelne Kapitel wieder – von intro, architecture und principles über schema, types und resolver bis zu mutations, subscriptions, paging, auth und caching. Dazu kommen ausführbare Beispiele: ein Apollo-Monorepo, ein Server in C# als Beleg dafür, dass GraphQL sprachunabhängig ist, und ein Ordner mit Rezepten für wiederkehrende Aufgaben – DataLoader, Paginierung, Auth, Optimistic UI, serverseitiges Rendern. Die Kapitel sind bewusst so geschnitten, dass jedes für sich lesbar ist, und genau so ist auch dieser Beitrag gedacht.
Was bleiben soll
Wenn von diesem Überblick ein Satz hängen bleiben soll, dann dieser: GraphQL ist zuerst eine Frage der API-Gestaltung und erst danach eine Frage des Werkzeugs. Das Schema ist der Vertrag, die Resolver sind die Umsetzung, und alles darüber – Federation, Caching, der konkrete Server – ist Ausführung dieser einen Idee, dass der Client seine Frage stellt und das Backend ein explizites Modell behält.
Die Tools werden sich weiter ändern; Apollo Server 4 wird nicht die letzte Generation sein, und die nächste kommt bestimmt. Was bleibt, ist das typisierte Schema und die Ausführung entlang des Graphen. Zu einzelnen Stationen gibt es eigene Beiträge – wie die Ausführung im Inneren genau abläuft und wie man ein Schema entwirft, das mitwächst.
Weiterführende Quellen
- Repository introduction-graphql (Kapitel, Apollo-Beispiel, Rezepte): https://github.com/mikebild/introduction-graphql
- Folienübersicht zu diesem Beitrag: https://supabase.mikebild.dev/storage/v1/object/public/slides/introduction-graphql.html
- GraphQL, offizielle Einführung: https://graphql.org/learn/
- GraphQL-Spezifikation: https://spec.graphql.org/
- GraphQL over HTTP: https://graphql.github.io/graphql-over-http/
- Apollo Server, Dokumentation: https://www.apollographql.com/docs/apollo-server
- DataLoader (Batching gegen N+1): https://github.com/graphql/dataloader
Wie fandest du diesen Beitrag?
Kommentare