Zum Inhalt springen
Vergleiche

GraphQL vs REST API - Wann welches einsetzen und wie wählen?

Veröffentlicht am:
·
Aktualisiert am:
·7 Min. Lesezeit·Autor: MDS Software Solutions Group

GraphQL REST API

porownania

GraphQL vs REST API - Ein umfassender Vergleich der API-Architekturen

Die Wahl der richtigen API-Architektur ist eine der grundlegendsten Entscheidungen in jedem Webprojekt. Ueber ein Jahrzehnt lang war REST (Representational State Transfer) der unbestrittene Standard fuer den Aufbau programmatischer Schnittstellen. 2015 veroeffentlichte Facebook GraphQL - eine Abfragesprache fuer APIs, die die Probleme loesen sollte, auf die sie bei der Entwicklung ihrer mobilen Anwendung gestossen waren. Heute haben beide Ansaetze ihren Platz im Oekosystem - aber wann sollte man welchen waehlen? In diesem Artikel vergleichen wir GraphQL und REST API ausfuehrlich aus jedem Blickwinkel.

Was ist eine REST API?#

REST ist ein Architekturstil, der auf Ressourcen basiert. Jede Ressource (z.B. Benutzer, Produkt, Bestellung) hat ihre eigene eindeutige URL, und Operationen werden mit Standard-HTTP-Methoden ausgefuehrt.

Grundprinzipien von REST#

  • Ressourcen identifiziert durch URLs - /api/users/1, /api/products/42
  • HTTP-Methoden als Operationen - GET (Lesen), POST (Erstellen), PUT/PATCH (Aktualisieren), DELETE (Loeschen)
  • Zustandslosigkeit - Jede Anfrage enthaelt alle Informationen, die fuer ihre Verarbeitung benoetigt werden
  • Einheitliche Schnittstelle - Konsistente Konventionen fuer alle Endpunkte
  • Cache-Schicht - Antworten koennen basierend auf HTTP-Headern zwischengespeichert werden

REST API Beispiel#

// Express.js Server - REST API
import express from 'express';

const app = express();
app.use(express.json());

// GET /api/users - Benutzerliste
app.get('/api/users', async (req, res) => {
  const users = await db.user.findMany();
  res.json(users);
});

// GET /api/users/:id - Benutzerdetails
app.get('/api/users/:id', async (req, res) => {
  const user = await db.user.findUnique({
    where: { id: Number(req.params.id) },
    include: { posts: true, profile: true },
  });
  if (!user) return res.status(404).json({ error: 'Nicht gefunden' });
  res.json(user);
});

// POST /api/users - Benutzer erstellen
app.post('/api/users', async (req, res) => {
  const user = await db.user.create({ data: req.body });
  res.status(201).json(user);
});

// PUT /api/users/:id - Benutzer aktualisieren
app.put('/api/users/:id', async (req, res) => {
  const user = await db.user.update({
    where: { id: Number(req.params.id) },
    data: req.body,
  });
  res.json(user);
});

// DELETE /api/users/:id - Benutzer loeschen
app.delete('/api/users/:id', async (req, res) => {
  await db.user.delete({ where: { id: Number(req.params.id) } });
  res.status(204).send();
});

Was ist GraphQL?#

GraphQL ist eine Abfragesprache und Laufzeitumgebung fuer APIs, die es dem Client ermoeglicht, genau festzulegen, welche Daten er benoetigt. Statt vieler Endpunkte gibt es einen einzigen Einstiegspunkt und ein flexibles Abfragesystem, das auf einem Typ-Schema basiert.

Grundkonzepte von GraphQL#

  • Schema und Typen - Vertrag zwischen Client und Server, der die Datenstruktur definiert
  • Abfragen (Queries) - Datenlesen mit praeziser Feldauswahl
  • Mutationen (Mutations) - Datenmodifizierende Operationen
  • Subskriptionen (Subscriptions) - Echtzeitdaten ueber WebSocket
  • Resolver - Funktionen, die definieren, wie Daten fuer jedes Feld abgerufen werden

GraphQL API Beispiel#

// GraphQL Schema
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
    profile: Profile
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    createdAt: String!
  }

  type Profile {
    id: ID!
    bio: String
    avatar: String
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts(limit: Int, offset: Int): [Post!]!
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!
  }

  input CreateUserInput {
    name: String!
    email: String!
    password: String!
  }

  input UpdateUserInput {
    name: String
    email: String
  }
`;

// Resolver
const resolvers = {
  Query: {
    users: () => db.user.findMany(),
    user: (_, { id }) => db.user.findUnique({ where: { id } }),
    posts: (_, { limit, offset }) =>
      db.post.findMany({ take: limit, skip: offset }),
  },
  Mutation: {
    createUser: (_, { input }) => db.user.create({ data: input }),
    updateUser: (_, { id, input }) =>
      db.user.update({ where: { id }, data: input }),
    deleteUser: async (_, { id }) => {
      await db.user.delete({ where: { id } });
      return true;
    },
  },
  User: {
    posts: (parent) =>
      db.post.findMany({ where: { authorId: parent.id } }),
    profile: (parent) =>
      db.profile.findUnique({ where: { userId: parent.id } }),
  },
};

Das Problem des Over-Fetching und Under-Fetching#

Dies ist wahrscheinlich das am haeufigsten genannte Argument fuer GraphQL.

Over-Fetching bei REST#

Over-Fetching tritt auf, wenn ein Endpunkt mehr Daten zurueckgibt, als der Client benoetigt.

// REST: GET /api/users/1
// Antwort enthaelt ALLE Felder, auch wenn nur der Name benoetigt wird
{
  "id": 1,
  "name": "Jan Kowalski",
  "email": "jan@example.com",
  "phone": "+48 123 456 789",
  "address": "ul. Przyk\u0142adowa 1, Warszawa",
  "bio": "Lorem ipsum dolor sit amet...",
  "avatar": "/uploads/jan.webp",
  "createdAt": "2024-01-15T10:30:00Z",
  "lastLogin": "2024-03-01T08:15:00Z",
  "preferences": { "theme": "dark", "language": "pl" }
}

Under-Fetching bei REST#

Under-Fetching ist die Situation, in der ein Endpunkt nicht genuegend Daten liefert und mehrere Anfragen noetig sind.

// Benoetigt: Benutzer + seine Beitraege + Kommentare zu den Beitraegen
// REST erfordert drei Anfragen:
const user = await fetch('/api/users/1');
const posts = await fetch('/api/users/1/posts');
const comments = await fetch('/api/posts/42/comments');

Die GraphQL-Loesung#

# Eine Abfrage - genau die Daten, die benoetigt werden
query {
  user(id: "1") {
    name
    posts {
      title
      comments {
        text
        author {
          name
        }
      }
    }
  }
}

GraphQL ermoeglicht es, genau die Felder und Beziehungen abzurufen, die benoetigt werden, in einer einzigen Anfrage. Dies ist ein enormer Vorteil, besonders auf mobilen Geraeten mit begrenzter Bandbreite.

Schema und Typisierung#

GraphQL - Schema-first vs Code-first#

GraphQL erzwingt die Definition eines Typ-Schemas, das einen Vertrag zwischen Frontend und Backend bildet.

// Code-first mit @nestjs/graphql
@ObjectType()
export class User {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field()
  email: string;

  @Field(() => [Post])
  posts: Post[];
}

@InputType()
export class CreateUserInput {
  @Field()
  @IsNotEmpty()
  name: string;

  @Field()
  @IsEmail()
  email: string;
}

Das GraphQL-Schema dient gleichzeitig als Dokumentation und Validierung - der Client weiss genau, welche Datentypen er erwarten kann.

REST - OpenAPI/Swagger#

REST hat kein eingebautes Typsystem, kann aber mit der OpenAPI-Spezifikation ergaenzt werden.

# openapi.yaml
paths:
  /api/users/{id}:
    get:
      summary: Benutzer abrufen
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email

Bei REST ist das Typ-Schema optional und erfordert ein zusaetzliches Tool, was in der Praxis oft vernachlaessigt wird.

API-Versionierung#

REST - Versionierung in URL oder Headern#

// Versionierung in der URL
app.get('/api/v1/users', handleUsersV1);
app.get('/api/v2/users', handleUsersV2);

// Versionierung in Headern
app.get('/api/users', (req, res) => {
  const version = req.headers['api-version'] || '1';
  if (version === '2') return handleUsersV2(req, res);
  return handleUsersV1(req, res);
});

Die Versionierung von REST APIs ist konzeptionell einfach, aber wartungsintensiv - jede neue Version bedeutet potenzielle Code-Duplikation und die Notwendigkeit, aeltere Versionen zu unterstuetzen.

GraphQL - Evolution ohne Versionen#

type User {
  id: ID!
  name: String!
  email: String!
  # Neues Feld - alte Clients rufen es einfach nicht ab
  phoneNumber: String
  # Feld als veraltet markieren
  phone: String @deprecated(reason: "Verwenden Sie phoneNumber")
}

GraphQL benoetigt keine Versionierung. Sie fuegen neue Felder hinzu und markieren alte als @deprecated. Clients rufen nur ab, was sie brauchen, sodass das Hinzufuegen von Feldern die Abwaertskompatibilitaet nicht bricht.

Caching#

REST - Natives HTTP-Caching#

REST funktioniert hervorragend mit HTTP-Cache-Schichten, da jede Ressource eine eindeutige URL hat.

// Cache-Header in REST
app.get('/api/products/:id', (req, res) => {
  res.set({
    'Cache-Control': 'public, max-age=3600',
    'ETag': `"product-${product.id}-${product.updatedAt}"`,
  });
  res.json(product);
});

// CDN, Browser und Proxys cachen Antworten automatisch
// GET /api/products/42 -> Cache HIT (gleiche URL = gleiche Ressource)

GraphQL - Caching erfordert zusaetzliche Arbeit#

Da GraphQL einen einzigen Endpunkt und POST-Anfragen verwendet, funktioniert traditionelles HTTP-Caching nicht.

// Apollo Client - normalisierter clientseitiger Cache
const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        keyFields: ['id'],
      },
      Post: {
        keyFields: ['id'],
        fields: {
          comments: {
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            },
          },
        },
      },
    },
  }),
});

// Persisted Queries - Konvertierung von POST zu GET mit Hash
// POST { query: "{ user(id: 1) { name } }" }
// wird zu:
// GET /graphql?extensions={"persistedQuery":{"sha256Hash":"abc123..."}}

Apollo Client bietet einen fortschrittlichen normalisierten Cache, der jedoch konfiguriert werden muss. REST gewinnt in Bezug auf die Einfachheit des Cachings.

Fehlerbehandlung#

REST - HTTP-Statuscodes#

// REST - Standard-HTTP-Codes
app.get('/api/users/:id', async (req, res) => {
  try {
    const user = await db.user.findUnique({
      where: { id: Number(req.params.id) },
    });
    if (!user) {
      return res.status(404).json({
        error: 'NOT_FOUND',
        message: 'Benutzer existiert nicht',
      });
    }
    res.json(user);
  } catch (error) {
    res.status(500).json({
      error: 'INTERNAL_ERROR',
      message: 'Interner Serverfehler',
    });
  }
});
// Client: Status 404 = Ressource existiert nicht, 401 = nicht autorisiert, usw.

GraphQL - Fehler im Antwort-Body#

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "Benutzer existiert nicht",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"],
      "extensions": {
        "code": "NOT_FOUND",
        "statusCode": 404
      }
    }
  ]
}
// Fehlerbehandlung in Resolvern
import { GraphQLError } from 'graphql';

const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await db.user.findUnique({ where: { id } });
      if (!user) {
        throw new GraphQLError('Benutzer existiert nicht', {
          extensions: { code: 'NOT_FOUND', statusCode: 404 },
        });
      }
      return user;
    },
  },
};

Bei GraphQL hat die Antwort fast immer den Status 200, und Fehler werden im errors-Feld uebermittelt. Dies kann fuer Monitoring-Tools verwirrend sein, die sich auf HTTP-Codes verlassen.

Das N+1-Problem#

Das N+1-Problem ist besonders relevant im Kontext von GraphQL, da der Client verschachtelte Beziehungen anfordern kann, was viele Datenbankabfragen generiert.

Das Problem#

# GraphQL-Abfrage
query {
  users {     # 1 SQL-Abfrage: SELECT * FROM users
    name
    posts {   # N SQL-Abfragen: SELECT * FROM posts WHERE author_id = ?
      title   # (eine Abfrage pro Benutzer)
    }
  }
}

Bei 100 Benutzern werden 101 Datenbankabfragen ausgefuehrt (1 + 100).

Loesung - DataLoader#

import DataLoader from 'dataloader';

// DataLoader buendelt Abfragen und fuehrt sie im Batch aus
const postLoader = new DataLoader(async (userIds: string[]) => {
  const posts = await db.post.findMany({
    where: { authorId: { in: userIds } },
  });
  const postsByUser = userIds.map((id) =>
    posts.filter((post) => post.authorId === id)
  );
  return postsByUser;
});

// Im Resolver
const resolvers = {
  User: {
    posts: (parent) => postLoader.load(parent.id),
  },
};

// Jetzt statt 101 Abfragen nur noch 2:
// SELECT * FROM users
// SELECT * FROM posts WHERE author_id IN (1, 2, 3, ..., 100)

DataLoader ist die Standardloesung fuer das N+1-Problem in GraphQL. Bei REST ist dieses Problem weniger gravierend, da der Server die Datenbankabfragen kontrolliert - der Client entscheidet nicht ueber die Verschachtelungstiefe.

Subskriptionen - Echtzeitdaten#

GraphQL Subscriptions#

// Schema
const typeDefs = `#graphql
  type Subscription {
    messageAdded(channelId: ID!): Message!
    userStatusChanged: UserStatus!
  }

  type Message {
    id: ID!
    text: String!
    author: User!
    createdAt: String!
  }
`;

// Subskriptions-Resolver
const resolvers = {
  Subscription: {
    messageAdded: {
      subscribe: (_, { channelId }) =>
        pubsub.asyncIterator([`MESSAGE_ADDED_${channelId}`]),
    },
  },
  Mutation: {
    sendMessage: async (_, { channelId, text }, { user }) => {
      const message = await db.message.create({
        data: { text, channelId, authorId: user.id },
      });
      pubsub.publish(`MESSAGE_ADDED_${channelId}`, {
        messageAdded: message,
      });
      return message;
    },
  },
};
// Client - Subskription in React mit Apollo
const MESSAGES_SUBSCRIPTION = gql`
  subscription OnMessageAdded($channelId: ID!) {
    messageAdded(channelId: $channelId) {
      id
      text
      author {
        name
        avatar
      }
      createdAt
    }
  }
`;

function ChatRoom({ channelId }) {
  const { data, loading } = useSubscription(MESSAGES_SUBSCRIPTION, {
    variables: { channelId },
  });

  // Neue Nachricht erscheint automatisch
  return <MessageList messages={data?.messageAdded} />;
}

Subskriptionen sind eine native Funktion von GraphQL, die REST ohne zusaetzliche Mechanismen (WebSocket, Server-Sent Events) nicht bietet.

Tools und Oekosystem#

GraphQL#

| Tool | Beschreibung | |------|-------------| | Apollo Client | Beliebtester GraphQL-Client mit Cache und State-Management | | Apollo Server | Produktions-GraphQL-Server fuer Node.js | | Relay | GraphQL-Client von Meta, optimiert fuer Performance | | GraphQL Code Generator | Automatische TypeScript-Typgenerierung aus dem Schema | | GraphiQL / Apollo Studio | Interaktive IDE zur API-Erkundung | | Hasura | Automatische GraphQL-API aus PostgreSQL-Datenbank | | Prisma | ORM mit nativer Unterstuetzung fuer GraphQL-Schema-Generierung |

REST#

| Tool | Beschreibung | |------|-------------| | Postman | Beliebtestes Tool zum API-Testen | | Swagger/OpenAPI | Standard zur Dokumentation von REST APIs | | Insomnia | Leichtgewichtige Alternative zu Postman | | cURL | Universelles Kommandozeilen-Tool | | Hoppscotch | Open-Source API-Test-Tool im Browser | | Express.js / Fastify | Beliebte HTTP-Frameworks | | json-server | Schneller Mock-REST-Server aus JSON-Datei |

Das REST-Oekosystem ist reifer und umfangreicher. GraphQL holt auf, und Tools wie Apollo und GraphQL Code Generator verbessern die Developer Experience erheblich.

Performance#

REST - Vorhersehbare Performance#

Bei REST hat jeder Endpunkt eine definierte Komplexitaet. Der Server kontrolliert, welche Daten zurueckgegeben werden, was die Optimierung erleichtert.

// REST - einfache Optimierung pro Endpunkt
app.get('/api/products', async (req, res) => {
  const products = await db.product.findMany({
    select: { id: true, name: true, price: true, thumbnail: true },
    take: 20,
  });
  res.json(products);
});

GraphQL - Flexibilitaet auf Kosten der Kontrolle#

Bei GraphQL entscheidet der Client ueber die Abfragetiefe, was zu komplexen Abfragen fuehren kann, die den Server belasten.

// Schutz vor komplexen Abfragen
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(5), // Maximale Verschachtelungstiefe
    createComplexityLimitRule(1000), // Komplexitaetslimit
  ],
});

GraphQL erfordert zusaetzliche Schutzmechanismen - Tiefenbegrenzung der Abfragen, Komplexitaetsanalyse und Rate-Limiting pro Abfrage (nicht pro Endpunkt wie bei REST).

Wann REST API waehlen?#

  • Einfache CRUD-Operationen - REST bildet Datenbankoperationen natuerlich ab
  • Intensives Caching - HTTP-Cache funktioniert nativ mit REST
  • Oeffentliche APIs - REST ist fuer externe Entwickler einfacher zu verstehen
  • Datei-Upload - REST unterstuetzt multipart/form-data ohne zusaetzliche Konfiguration
  • Microservices mit einfachen Vertraegen - REST ist leichter und weniger komplex
  • Team ohne GraphQL-Erfahrung - Die Lernkurve von REST ist sanfter
  • API mit wenigen Clients - Wenn Over-Fetching kein Problem darstellt

Wann GraphQL waehlen?#

  • Viele Clients mit unterschiedlichen Beduerfnissen - Mobil, Web, IoT - jeder ruft andere Daten ab
  • Komplexe Datenbeziehungen - Verschachtelte Abfragen eliminieren viele HTTP-Anfragen
  • Sich schnell entwickelnde APIs - Felder hinzufuegen ohne Versionierung
  • Echtzeit-Anwendungen - Eingebaute Subskriptionen
  • Aggregation mehrerer Services - GraphQL als API Gateway (Federation)
  • Developer Experience - Autovervollstaendigung, Typisierung, interaktive Dokumentation
  • Mobile Anwendungen - Minimierung des Datentransfers ist entscheidend

Hybrider Ansatz#

In der Praxis kombinieren viele Teams beide Ansaetze und nutzen ihre jeweiligen Staerken.

                      +-----------------------+
                      |   API Gateway         |
                      |   (GraphQL)           |
                      +----------+------------+
                                 |
              +------------------+------------------+
              |                  |                  |
        +-----v------+  +-------v------+  +-------v------+
        | User       |  | Product      |  | Order        |
        | Service    |  | Service      |  | Service      |
        | (REST)     |  | (REST)       |  | (REST)       |
        +------------+  +--------------+  +--------------+
// GraphQL als Aggregationsschicht fuer REST-Microservices
const resolvers = {
  Query: {
    orderDetails: async (_, { orderId }) => {
      const [order, user, products] = await Promise.all([
        fetch(`http://orders-service/api/orders/${orderId}`).then(r => r.json()),
        fetch(`http://users-service/api/users/${order.userId}`).then(r => r.json()),
        fetch(`http://products-service/api/products?ids=${order.productIds.join(',')}`).then(r => r.json()),
      ]);
      return { ...order, user, products };
    },
  },
};

Ein beliebtes Muster ist GraphQL Federation (Apollo Federation), bei dem jeder Microservice seinen Teil des GraphQL-Schemas definieren kann und das Gateway sie zu einem einheitlichen Schema zusammenfuegt.

Vergleichstabelle#

| Aspekt | REST | GraphQL | |--------|------|---------| | Architektur | Viele Endpunkte, Ressourcen | Ein Endpunkt, Abfragen | | Datenabruf | Feste Antworten pro Endpunkt | Client waehlt genaue Felder | | Over-Fetching | Haeufig | Keines | | Under-Fetching | Haeufig (mehrere Anfragen) | Keines (eine Abfrage) | | Typisierung | Optional (OpenAPI) | Eingebaut (Schema) | | Versionierung | URL oder Header | Schema-Evolution | | Caching | Natives HTTP-Cache | Erfordert Apollo/Relay | | Fehler | HTTP-Statuscodes | errors-Feld in der Antwort | | Echtzeit | SSE/WebSocket (zusaetzlich) | Eingebaute Subskriptionen | | Datei-Upload | Natives Multipart | Erfordert zusaetzliche Konfiguration | | Lernkurve | Niedrig | Mittel-hoch | | Tools | Postman, cURL, Swagger | Apollo Studio, GraphiQL | | Performance | Vorhersehbar | Erfordert Komplexitaetsschutz |

Zusammenfassung#

Es gibt keine eindeutige Antwort auf die Frage "GraphQL oder REST?". Beide Ansaetze haben ihre Staerken und Anwendungsbereiche. REST bleibt eine ausgezeichnete Wahl fuer einfache, klar definierte APIs mit intensivem Caching und oeffentlichem Zugang. GraphQL eignet sich am besten fuer komplexe Anwendungen mit vielen Clients, bei denen Flexibilitaet beim Datenabruf und starke Typisierung von entscheidender Bedeutung sind.

Der Markttrend zeigt eine zunehmende Adoption von GraphQL, insbesondere in Frontend- und mobilen Anwendungen. Gleichzeitig verschwindet REST nicht - beide Technologien werden koexistieren, und die erfolgreichsten Teams koennen beide dort einsetzen, wo sie am besten funktionieren.


Benoetigen Sie eine professionelle API fuer Ihr Projekt?#

Bei MDS Software Solutions Group entwerfen und implementieren wir leistungsfaehige APIs sowohl in REST- als auch in GraphQL-Architektur. Unser Team hilft Ihnen, den optimalen Ansatz zu waehlen, das Datenschema zu entwerfen und eine skalierbare Loesung zu entwickeln, die auf die Beduerfnisse Ihres Unternehmens zugeschnitten ist.

Kontaktieren Sie uns, um Ihr Projekt zu besprechen - von der Anforderungsanalyse und Architekturberatung bis hin zur vollstaendigen Implementierung und Wartung der API.

Autor
MDS Software Solutions Group

Team von Programmierexperten, die sich auf moderne Webtechnologien spezialisiert haben.

GraphQL vs REST API - Wann welches einsetzen und wie wählen? | MDS Software Solutions Group | MDS Software Solutions Group