API-Design-Patterns — REST, gRPC und GraphQL in der Praxis
Die falsche Frage
„Sollen wir REST, GraphQL oder gRPC verwenden?“ ist die falsche Frage. Die richtige lautet: Was braucht Dein System tatsächlich?
Jeder API-Stil existiert, weil er eine bestimmte Problemklasse gut löst. Der häufigste Fehler: Ein Pattern für alles wählen — oder schlimmer, basierend auf dem aktuellen Hacker-News-Trend entscheiden.
Hier ist ein Entscheidungs-Framework aus echten Projekten, nicht aus Blog-Hype.
REST: Der Default, der selten falsch ist
REST ist die HTTP-native Wahl. Es basiert auf Standards, die jeder kennt: URLs für Ressourcen, HTTP-Methoden für Aktionen, Statuscodes für Ergebnisse. Das ist seine Stärke.
GET /api/v1/users/42
POST /api/v1/users
PUT /api/v1/users/42
DELETE /api/v1/users/42
Wann REST die richtige Wahl ist:
- Öffentliche APIs, bei denen Developer Experience zählt
- CRUD-lastige Domänen (Content-Management, Admin-Panels, E-Commerce)
- Teams, die schnell neue Entwickler onboarden müssen
- Systeme, bei denen Caching wichtig ist (HTTP-Caching gibt’s bei REST gratis)
Wo REST an Grenzen stößt:
- Komplexe, verschachtelte Datenanforderungen (das klassische Over-Fetching/Under-Fetching-Problem)
- Hochperformante interne Service-Kommunikation
- Szenarien, in denen Bandbreite wichtiger ist als Entwickler-Ergonomie
Die OpenAPI-Spezifikation ist hier Dein bester Freund. Definiere den API-Vertrag zuerst, generiere Clients und Server-Stubs daraus. Documentation-First-API-Design eliminiert die gesamte Klasse von „die API macht etwas leicht anderes als abgesprochen“-Bugs.
Fehlerbehandlung in REST
Die meisten REST-APIs machen Fehlerbehandlung falsch. Sie liefern 200 für alles und packen Fehlercodes in den Body. Das ist keine gute Idee.
// Schlecht: 200 OK mit einem Fehler im Body
{ "success": false, "error": "User not found" }
// Gut: saubere HTTP-Semantik
// 404 Not Found
{
"type": "https://api.example.com/errors/not-found",
"title": "User not found",
"status": 404,
"detail": "No user with ID 42 exists.",
"instance": "/api/v1/users/42"
}
Das zweite Format folgt RFC 9457 (Problem Details for HTTP APIs). Es ist ein Standard. Nutze ihn.
gRPC: Wenn Performance nicht verhandelbar ist
gRPC nutzt HTTP/2 und Protocol Buffers für binäre Serialisierung. Es ist schnell — deutlich schneller als JSON über REST für die Service-zu-Service-Kommunikation.
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
}
message GetUserRequest {
string id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
Wann gRPC die richtige Wahl ist:
- Interne Microservice-Kommunikation, bei der Latenz zählt
- Streaming-Anwendungen (Echtzeit-Datenfeeds, Log-Streaming)
- Polyglotte Umgebungen — gRPC generiert Clients in dutzenden Sprachen
- Mobile Clients in eingeschränkten Netzwerken (binär ist kleiner als JSON)
Wo gRPC an Grenzen stößt:
- Browser-Clients (gRPC-Web existiert, erhöht aber die Komplexität)
- Einfache CRUD-APIs, bei denen der Protocol-Buffers-Overhead sich nicht lohnt
- Teams, die mit Protobuf-Tooling nicht vertraut sind
Die Contract-First-Natur von .proto-Dateien ist ein großer Vorteil. Dein API-Vertrag ist die Protobuf-Definition — sie kompiliert zu typsicheren Clients, und Breaking Changes werden beim Build erkannt.
GraphQL: Wenn der Client die Daten bestimmt
GraphQL kehrt das Kontrollmodell um. Statt dass der Server entscheidet, welche Daten zurückkommen, spezifiziert der Client exakt, was er braucht.
query {
user(id: "42") {
name
email
orders(last: 5) {
id
total
items {
name
}
}
}
}
Ein Request. Genau die Daten, die das UI braucht. Kein Over-Fetching.
Wann GraphQL die richtige Wahl ist:
- Komplexe Frontends mit variierenden Datenanforderungen (Dashboards, Mobile Apps)
- Mehrere Client-Typen am selben Backend (Web, iOS, Android wollen jeweils andere Felder)
- Schnelle Frontend-Iteration, bei der das Backend nicht der Flaschenhals sein soll
Wo GraphQL an Grenzen stößt:
- Einfache APIs mit vorhersagbaren Datenstrukturen
- Datei-Uploads (möglich, aber umständlich)
- Caching (kein HTTP-Caching von Haus aus — Du brauchst Persisted Queries oder eine CDN-Schicht)
- Autorisierung auf Feldebene wird schnell komplex
Das N+1-Problem ist GraphQLs größte operative Falle. Jeder Resolver kann zusätzliche Datenbankabfragen auslösen. Nutze DataLoader-Patterns oder Tools wie graphql-batch, um Anfragen zu bündeln.
Der Hybrid-Ansatz: So sehen Produktivsysteme wirklich aus
In der Praxis nutzen die meisten ernsthaften Systeme mehr als ein Pattern. Eine typische Aufteilung:
| Schicht | Pattern | Warum |
|---|---|---|
| Öffentliche API | REST + OpenAPI | Developer Experience, Caching, breites Tooling |
| Interne Services | gRPC | Performance, Typsicherheit, Streaming |
| Frontend-Gateway | GraphQL | Client-gesteuerte Queries, Aggregation |
Das ist kein Over-Engineering — es bedeutet, das richtige Werkzeug für jede Schicht zu verwenden. Das API-Gateway-Pattern verbindet alles und präsentiert eine einheitliche Schnittstelle, während jeder Backend-Service intern das passende Protokoll nutzt.
Versionierung: Der Teil, über den niemand nachdenken will
APIs entwickeln sich weiter. Clients gehen kaputt. Versionierung ist, wie Du diese Spannung managst.
Drei gängige Strategien:
- URI-Versionierung (
/v1/users,/v2/users) — einfach, sichtbar, führt aber zu URL-Wildwuchs - Header-Versionierung (
Accept: application/vnd.api+json;version=2) — sauberere URLs, schwerer im Browser zu testen - Query-Parameter (
/users?version=2) — leicht zu implementieren, fühlt sich aber wie ein Hack an
Unsere Empfehlung: URI-Versionierung für Major Breaking Changes, additive Änderungen ohne Versionierung.
Die zentrale Erkenntnis aus Stripes API-Versionierungsansatz: Die meisten Änderungen sollten abwärtskompatible Ergänzungen sein. Neue Felder, neue Endpunkte, neue optionale Parameter. Versionsbumps nur für echte Breaking Changes.
Praktische Regeln:
- Ein neues Feld in einer Response ist nie ein Breaking Change
- Ein neuer optionaler Request-Parameter ist nie ein Breaking Change
- Ein Feld entfernen oder umbenennen IST ein Breaking Change
- Den Typ eines Feldes ändern IST ein Breaking Change
Documentation-First: Das Pattern, das Schmerzen verhindert
Schreibe die API-Spec, bevor Du den Code schreibst. Nutze OpenAPI für REST, .proto-Dateien für gRPC oder SDL für GraphQL.
Das hat nichts mit Bürokratie zu tun. Es geht darum, Design-Probleme zu finden, bevor sie zu Code-Problemen werden. Es ist billiger, über eine YAML-Datei zu diskutieren, als drei Services und fünf Clients umzubauen.
Tools, die das praktikabel machen:
- Redocly für OpenAPI-Linting und Dokumentation
- buf für Protobuf-Linting und Breaking-Change-Erkennung
- GraphQL Inspector für Schema-Change-Tracking
Die Entscheidungs-Checkliste
Bevor Du ein Pattern wählst, beantworte diese Fragen:
- Wer konsumiert die API? Externe Entwickler → REST. Interne Services → gRPC. Komplexes Frontend → GraphQL.
- Wie sieht die Datenstruktur aus? Vorhersagbar → REST. Variiert pro Client → GraphQL. Binär/Streaming → gRPC.
- Was kann das Team? Nimm das, was das Team kennt — es sei denn, es gibt einen zwingenden technischen Grund dagegen.
- Was sind die Performance-Anforderungen? Sub-Millisekunden-Inter-Service-Calls → gRPC. Standard-Web-Latenz → REST oder GraphQL.
- Wie wird sich die API entwickeln? Plane Versionierung von Tag eins. Additive Änderungen sind immer sicherer als Breaking Changes.
Fazit
Es gibt kein universell „bestes“ API-Pattern. Es gibt nur das beste Pattern für Deine spezifischen Anforderungen. REST ist der sichere Default. gRPC gewinnt bei Performance. GraphQL gewinnt bei Flexibilität. Die meisten Produktivsysteme nutzen einen Mix.
Entscheide basierend auf Anforderungen, nicht auf Hype. Dokumentiere den Vertrag zuerst. Versioniere von Tag eins. Und dann: ausliefern.