GraphQL vs. REST: den richtigen API-Stil wählen
GraphQL ist kein Ersatz für REST — es ist eine Alternative mit anderen Trade-offs, die je nach Client-Komplexität, Teamgröße und Datenform relevant sind.

Die Debatte GraphQL vs. REST erzeugt oft mehr Hitze als Erkenntnis. Teams führen GraphQL ein, weil es angesagt ist, und kämpfen dann mit Caching, Autorisierung und N+1-Queries. Andere lehnen es komplett ab und bauen REST-Endpoints mit 15 Query-Parametern, um Over-Fetching zu vermeiden. Die richtige Wahl hängt von deinen spezifischen Rahmenbedingungen ab — Client-Vielfalt, Datenbeziehungen und Team-Expertise.
Der Kernunterschied
REST organisiert APIs um Ressourcen mit festen Antwortformen. GraphQL gibt Clients eine Abfragesprache, um genau die Daten anzufordern, die sie brauchen.
// REST — multiple requests, potentially over-fetching
// GET /api/users/123
// GET /api/users/123/orders?limit=5
// GET /api/users/123/reviews?limit=3
// Each endpoint returns its full schema, including fields the client doesn't use
// GraphQL — one request, exact data
const query = `
query UserDashboard($userId: ID!) {
user(id: $userId) {
name
avatar
orders(limit: 5) {
id
total
status
}
reviews(limit: 3) {
rating
comment
}
}
}
`;Bei REST entscheidet der Server, welche Daten zurückgegeben werden. Bei GraphQL entscheidet der Client. Das verlagert Komplexität vom Client auf den Server.
Wo REST gewinnt
REST ist einfacher für unkomplizierte CRUD-Operationen mit klar definierten Ressourcen.
// ✅ REST excels at simple, resource-oriented APIs
// Clear, cacheable, easy to understand
app.get("/api/products/:id", getProduct);
app.post("/api/products", createProduct);
app.patch("/api/products/:id", updateProduct);
app.delete("/api/products/:id", deleteProduct);
// HTTP caching works out of the box
// GET /api/products/123
// Cache-Control: public, max-age=300
// ETag: "abc123"// ❌ GraphQL adds complexity for simple operations
const CREATE_PRODUCT = gql`
mutation CreateProduct($input: CreateProductInput!) {
createProduct(input: $input) {
id
name
price
}
}
`;
// Every request is POST, every response is 200
// HTTP caching doesn't work without additional infrastructure
// Error handling is an envelope format, not HTTP status codesVorteile von REST: HTTP-Caching, einfacheres Tooling, Statuscode-Semantik, kleinere Lernkurve, CDN-Kompatibilität.
Wo GraphQL gewinnt
GraphQL glänzt, wenn Clients unterschiedliche Datenbedürfnisse haben — besonders bei mobilen Apps mit Bandbreitenbeschränkungen oder Dashboards, die Daten aus mehreren Domänen aggregieren.
// ❌ REST — the mobile app needs 5 requests for one screen
const user = await fetch("/api/users/me");
const orders = await fetch("/api/users/me/orders?limit=3");
const notifications = await fetch("/api/notifications?unread=true");
const recommendations = await fetch("/api/recommendations?limit=5");
const stats = await fetch("/api/users/me/stats");
// 5 round trips, over-fetching on each response
// ✅ GraphQL — one request, exact data needed
const DASHBOARD = gql`
query Dashboard {
me {
name
avatar
}
myOrders(limit: 3) {
id
total
status
}
notifications(filter: { unread: true }) {
id
message
}
recommendations(limit: 5) {
id
title
image
}
myStats {
totalOrders
totalSpent
}
}
`;Ein Request ersetzt fünf. Der Client bekommt genau die Felder, die er braucht, nicht mehr. In mobilen Netzwerken zählt das.
Das N+1-Problem in GraphQL
GraphQLs flexible Queries schaffen eine serverseitige Herausforderung: Das Auflösen verschachtelter Felder kann Hunderte von Datenbankabfragen auslösen.
// This innocent query triggers 1 + N database calls
// 1 query for users, then N queries for each user's orders
const query = `{
users(limit: 50) {
name
orders {
id
total
}
}
}`;// ✅ DataLoader batches and deduplicates database calls
import DataLoader from "dataloader";
const orderLoader = new DataLoader(async (userIds: string[]) => {
// One query for ALL user orders, not one per user
const orders = await db.orders.findMany({
where: { userId: { in: userIds } },
});
// Group by userId and return in the same order as input
const ordersByUser = new Map<string, Order[]>();
for (const order of orders) {
const list = ordersByUser.get(order.userId) ?? [];
list.push(order);
ordersByUser.set(order.userId, list);
}
return userIds.map((id) => ordersByUser.get(id) ?? []);
});
// In the resolver
const resolvers = {
User: {
orders: (user: User) => orderLoader.load(user.id),
},
};DataLoader ist für jeden nicht-trivialen GraphQL-Server unverzichtbar. Ohne ihn verschlechtert sich die Performance exponentiell mit der Query-Tiefe.
Komplexität der Autorisierung
Autorisierung bei REST ist unkompliziert — Middleware prüft, bevor der Handler läuft. Bei GraphQL muss Autorisierung auf Feldebene stattfinden, weil unterschiedliche Felder unterschiedliche Zugriffsregeln haben können.
// REST — one auth check per endpoint
app.get("/api/users/:id", requireAuth, requireOwnerOrAdmin, getUser);
// GraphQL — auth at the field level
const resolvers = {
User: {
email: (user, args, context) => {
// Only the user themselves or an admin can see email
if (context.userId !== user.id && !context.isAdmin) return null;
return user.email;
},
salary: (user, args, context) => {
// Only HR or the user themselves
if (context.userId !== user.id && !context.roles.includes("hr")) {
throw new ForbiddenError("Cannot access salary");
}
return user.salary;
},
},
};Diese feldweise Autorisierung ist granularer, aber schwerer zu auditieren. "Wer darf was sehen?" wird zu einer Frage, die das Nachverfolgen von Resolver-Code erfordert, statt einfach die Route-Middleware zu überfliegen.
Entscheidungsrahmen
| Faktor | REST wählen | GraphQL wählen |
|---|---|---|
| Client-Vielfalt | 1-2 Clients mit ähnlichen Bedürfnissen | Mehrere Clients mit unterschiedlichen Datenbedürfnissen |
| Datenform | Flach, ressourcenorientiert | Tief verschachtelt, relational |
| Caching-Bedarf | HTTP-Caching wichtig | Individuelles Caching akzeptabel |
| Teamgröße | Kleines Team, einfache API | Größeres Team, dedizierte API-Schicht |
| Echtzeit-Bedarf | SSE oder WebSocket separat | Subscriptions eingebaut |
| API-Konsumenten | Externe Entwickler | Interne Clients, die du kontrollierst |
Die wichtigsten Punkte
- REST ist einfacher für ressourcenorientiertes CRUD mit guter HTTP-Caching-Unterstützung
- GraphQL reduziert Over-Fetching, wenn Clients unterschiedliche, verschachtelte Datenbedürfnisse haben
- DataLoader ist Pflicht für jeden GraphQL-Server, um N+1-Query-Probleme zu vermeiden
- GraphQL verlagert Komplexität auf den Server — Autorisierung, Caching und Performance werden schwieriger
- Wähle nicht nach Trends — wähle nach Client-Vielfalt, Datenform und Team-Expertise


