Zum Inhalt springen

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.

3 Min. Lesezeit
Gegenüberstellung einer GraphQL-Query und eines REST-Endpoints für dieselben Daten

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.

tstypescript
// 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.

tstypescript
// ✅ 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"
tstypescript
// ❌ 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 codes

Vorteile 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.

tstypescript
// ❌ 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.

tstypescript
// 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
    }
  }
}`;
tstypescript
// ✅ 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.

tstypescript
// 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

FaktorREST wählenGraphQL wählen
Client-Vielfalt1-2 Clients mit ähnlichen BedürfnissenMehrere Clients mit unterschiedlichen Datenbedürfnissen
DatenformFlach, ressourcenorientiertTief verschachtelt, relational
Caching-BedarfHTTP-Caching wichtigIndividuelles Caching akzeptabel
TeamgrößeKleines Team, einfache APIGrößeres Team, dedizierte API-Schicht
Echtzeit-BedarfSSE oder WebSocket separatSubscriptions eingebaut
API-KonsumentenExterne EntwicklerInterne Clients, die du kontrollierst

Die wichtigsten Punkte

  1. REST ist einfacher für ressourcenorientiertes CRUD mit guter HTTP-Caching-Unterstützung
  2. GraphQL reduziert Over-Fetching, wenn Clients unterschiedliche, verschachtelte Datenbedürfnisse haben
  3. DataLoader ist Pflicht für jeden GraphQL-Server, um N+1-Query-Probleme zu vermeiden
  4. GraphQL verlagert Komplexität auf den Server — Autorisierung, Caching und Performance werden schwieriger
  5. Wähle nicht nach Trends — wähle nach Client-Vielfalt, Datenform und Team-Expertise
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX