ToolBox Hub
Detailansicht von Programmiercode in einem dunklen Theme auf einem Computerbildschirm.

REST API vs GraphQL vs tRPC in 2026: Welches sollten Sie verwenden?

đŸ“· Stanislav Kondratiev / Pexels

REST API vs GraphQL vs tRPC in 2026: Welches sollten Sie verwenden?

Ein detaillierter Vergleich von REST, GraphQL und tRPC fuer moderne Webentwicklung. Lernen Sie die Vor- und Nachteile, Leistungsmerkmale und besten Einsatzbereiche jedes Ansatzes kennen.

19. MĂ€rz 202612 Min. Lesezeit

Die Wahl, wie Ihr Frontend mit Ihrem Backend kommuniziert, ist eine der folgenreichsten architektonischen Entscheidungen in einem Webprojekt. Im Jahr 2026 haben Entwickler drei ausgereifte, praxiserprobte Optionen: REST, GraphQL und tRPC. Jede hat deutliche Staerken, Kompromisse und ideale Einsatzszenarien. Dieser Artikel bietet einen gruendlichen Vergleich, der Ihnen hilft, die richtige Wahl fuer Ihr naechstes Projekt zu treffen.

REST API: Der etablierte Standard

REST (Representational State Transfer) ist seit ueber zwei Jahrzehnten das dominierende API-Paradigma. Es bildet HTTP-Methoden auf CRUD-Operationen an Ressourcen ab und ist dadurch intuitiv und von praktisch jedem Entwickler verstanden.

Wie REST funktioniert

REST APIs stellen Ressourcen als URLs bereit. Clients interagieren mit ihnen ueber Standard-HTTP-Methoden:

// GET eine Liste von Benutzern
const response = await fetch('https://api.example.com/users');
const users = await response.json();

// GET einen einzelnen Benutzer
const user = await fetch('https://api.example.com/users/42');

// POST um einen neuen Benutzer zu erstellen
const newUser = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Alice Johnson',
    email: 'alice@example.com',
  }),
});

// PUT um einen Benutzer zu aktualisieren
await fetch('https://api.example.com/users/42', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice Smith' }),
});

// DELETE einen Benutzer
await fetch('https://api.example.com/users/42', {
  method: 'DELETE',
});

Serverseitiges REST-Beispiel (Express)

import express from 'express';

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

// GET /users
app.get('/users', async (req, res) => {
  const { page = 1, limit = 20, sort = 'name' } = req.query;
  const users = await db.users.findMany({
    skip: (Number(page) - 1) * Number(limit),
    take: Number(limit),
    orderBy: { [sort as string]: 'asc' },
  });
  res.json({ data: users, page: Number(page), limit: Number(limit) });
});

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

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

Staerken von REST

  • Universell verstanden: Jeder Entwickler, jede Sprache, jede Plattform unterstuetzt REST.
  • HTTP-Caching: GET-Anfragen koennen von Browsern, CDNs und Proxies sofort gecacht werden.
  • Zustandslos und einfach: Jede Anfrage enthaelt alle Informationen, die der Server benoetigt.
  • Ausgereiftes Tooling: Swagger/OpenAPI, Postman und unzaehlige Bibliotheken.
  • Statuscodes sind aussagekraeftig: 200, 201, 404, 422, 500 vermitteln jeweils spezifische Informationen.

Schwaechen von REST

  • Over-Fetching: Ein Endpoint liefert alle Felder zurueck, auch wenn der Client nur wenige benoetigt.
  • Under-Fetching: Das Abrufen verwandter Daten erfordert oft mehrere Roundtrips.
  • Keine eingebaute Typsicherheit: Ohne zusaetzliches Tooling wie OpenAPI-Codegenerierung gibt es keinen Vertrag zwischen Client und Server.
  • Versionierungsherausforderungen: Breaking Changes erfordern oft /v2/-Endpoints oder Header-basierte Versionierung.

GraphQL: Flexible Abfragen

GraphQL, von Facebook entwickelt und 2015 als Open Source veroeffentlicht, ermoeglicht es dem Client, in einer einzigen Anfrage genau die benoetigten Daten anzugeben. Es verwendet ein typisiertes Schema, um die API-Oberflaeche zu definieren.

Wie GraphQL funktioniert

// Eine GraphQL-Abfrage
const query = `
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      email
      posts(first: 5) {
        title
        createdAt
      }
      followers {
        totalCount
      }
    }
  }
`;

const response = await fetch('https://api.example.com/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    query,
    variables: { id: '42' },
  }),
});

const { data } = await response.json();
// data.user.name, data.user.posts, data.user.followers.totalCount

Serverseitiges GraphQL-Schema

// schema.ts (mit Schema-first-Ansatz)
const typeDefs = `
  type User {
    id: ID!
    name: String!
    email: String!
    posts(first: Int, after: String): PostConnection!
    followers: FollowerCount!
  }

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

  type PostConnection {
    edges: [PostEdge!]!
    pageInfo: PageInfo!
  }

  type Query {
    user(id: ID!): User
    users(first: Int, after: String): UserConnection!
    post(id: ID!): Post
  }

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

  type Subscription {
    postCreated: Post!
  }
`;

Resolver

const resolvers = {
  Query: {
    user: async (_: unknown, { id }: { id: string }, ctx: Context) => {
      return ctx.db.users.findUnique({ where: { id } });
    },
    users: async (_: unknown, args: PaginationArgs, ctx: Context) => {
      return paginateUsers(ctx.db, args);
    },
  },
  User: {
    posts: async (parent: User, args: PaginationArgs, ctx: Context) => {
      return ctx.loaders.userPosts.load({ userId: parent.id, ...args });
    },
    followers: async (parent: User, _: unknown, ctx: Context) => {
      const count = await ctx.db.followers.count({
        where: { followeeId: parent.id },
      });
      return { totalCount: count };
    },
  },
  Mutation: {
    createUser: async (_: unknown, { input }: { input: CreateUserInput }, ctx: Context) => {
      return ctx.db.users.create({ data: input });
    },
  },
};

Staerken von GraphQL

  • Kein Over-Fetching oder Under-Fetching: Clients fordern genau die Felder an, die sie benoetigen.
  • Ein einziger Endpoint: Alle Operationen gehen ueber eine URL.
  • Stark typisiertes Schema: Das Schema dient als lebende Dokumentation und ermoeglicht leistungsfaehiges Tooling.
  • Introspection: Clients koennen die gesamte API-Oberflaeche programmatisch entdecken.
  • Subscriptions: Eingebaute Unterstuetzung fuer Echtzeitdaten ueber WebSockets.
  • Ideal fuer verschiedene Clients: Eine mobile App kann minimale Felder anfordern, waehrend eine Desktop-App alles abruft.

Schwaechen von GraphQL

  • Komplexitaet: Resolver, Data Loader, Persisted Queries und Schema-Management erhoehen den kognitiven Aufwand.
  • Caching ist schwieriger: Da alle Anfragen POSTs an einen einzigen Endpoint sind, funktioniert traditionelles HTTP-Caching nicht. Sie benoetigen clientseitiges Caching (Apollo, urql) oder Persisted Queries.
  • N+1-Abfrageproblem: Ohne Data Loader koennen verschachtelte Resolver uebermassig viele Datenbankabfragen ausloesen.
  • Datei-Uploads: Nicht nativ unterstuetzt; erfordert Multipart-Request-Spezifikationen oder separate Upload-Endpoints.
  • Sicherheitsoberflaeche: Tief verschachtelte oder aufwendige Abfragen koennen Denial-of-Service verursachen, wenn Sie keine Abfragetiefenbegrenzung und Komplexitaetsanalyse implementieren.

tRPC: End-to-End-Typsicherheit

tRPC ist ein TypeScript-spezifisches Framework, mit dem Sie Serverfunktionen direkt vom Client aus mit voller Typsicherheit und ohne Codegenerierung aufrufen koennen. Es ist fuer Monorepo-Projekte konzipiert, bei denen sowohl Frontend als auch Backend in TypeScript geschrieben sind.

Wie tRPC funktioniert

tRPC eliminiert die API-Schicht als separates Konzept. Sie definieren Prozeduren auf dem Server, und der Client ruft sie auf, als waeren es lokale Funktionen.

Serverseitige Router-Definition:

// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) => {
      const user = await ctx.db.users.findUnique({
        where: { id: input.id },
        include: { posts: true, profile: true },
      });
      if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
      return user;
    }),

  list: publicProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().min(1).max(100).default(20),
      search: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const { page, limit, search } = input;
      const where = search
        ? { name: { contains: search, mode: 'insensitive' as const } }
        : {};
      const [users, total] = await Promise.all([
        ctx.db.users.findMany({
          where,
          skip: (page - 1) * limit,
          take: limit,
        }),
        ctx.db.users.count({ where }),
      ]);
      return { users, total, page, limit };
    }),

  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1).max(100),
      email: z.string().email(),
    }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.users.create({ data: input });
    }),

  update: protectedProcedure
    .input(z.object({
      id: z.string(),
      name: z.string().min(1).max(100).optional(),
      email: z.string().email().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { id, ...data } = input;
      return ctx.db.users.update({ where: { id }, data });
    }),
});

Clientseitige Verwendung (mit React):

// Client: volle Typsicherheit, keine Codegenerierung noetig
import { trpc } from '../utils/trpc';

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = trpc.user.getById.useQuery({ id: userId });

  const updateUser = trpc.user.update.useMutation({
    onSuccess: () => {
      // Invalidieren und neu abrufen
      utils.user.getById.invalidate({ id: userId });
    },
  });

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage error={error.message} />;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={() => updateUser.mutate({ id: userId, name: 'New Name' })}>
        Name aktualisieren
      </button>
    </div>
  );
}

Jeder Input, Output und Fehler ist vollstaendig typisiert. Wenn Sie einen Feldnamen auf dem Server aendern, zeigt der Client sofort einen TypeScript-Fehler an.

Staerken von tRPC

  • End-to-End-Typsicherheit: Aenderungen auf dem Server werden sofort in den Client-Typen widergespiegelt -- ohne Codegenerierung.
  • Minimaler Boilerplate: Keine Schema-Dateien, keine API-Route-Definitionen, kein Client-Generierungsschritt.
  • Hervorragende Entwicklererfahrung: Autovervollstaendigung, Inline-Dokumentation und Refactoring-Unterstuetzung in Ihrer IDE.
  • Eingebaute Input-Validierung: Zod-Schemas validieren Inputs zur Laufzeit und liefern Typen zur Kompilierzeit.
  • React Query-Integration: Erstklassige Unterstuetzung fuer Caching, optimistische Updates und Echtzeit-Refetching.
  • Subscriptions: WebSocket-Unterstuetzung fuer Echtzeit-Features.

Schwaechen von tRPC

  • Nur TypeScript: Sowohl Client als auch Server muessen TypeScript sein. Es funktioniert nicht mit Python, Go oder anderen Backend-Sprachen.
  • Monorepo-zentriert: Funktioniert am besten, wenn Client und Server eine Codebasis teilen. Weniger praktisch, wenn Teams in separaten Repositories arbeiten.
  • Kein Public-API-Anwendungsfall: Nicht geeignet fuer APIs, die von Dritten konsumiert werden, da es kein sprachunabhaengiges Schema gibt.
  • Gekoppelter Client und Server: Enge Kopplung kann bei wachsenden Teams zu Wartungsproblemen fuehren.
  • Kleineres Oekosystem: Weniger Tutorials, Tools und Community-Ressourcen im Vergleich zu REST oder GraphQL.

Vergleichstabelle

FeatureRESTGraphQLtRPC
TypsicherheitManuell (OpenAPI Codegen)Schema-basiertAutomatisch (TypeScript)
Over-FetchingHaeufigEliminiertMinimal (typisierte Returns)
Under-FetchingHaeufig (N+1-Anfragen)EliminiertMinimal
CachingHervorragend (HTTP-nativ)Komplex (clientseitig)React Query eingebaut
LernkurveNiedrigMittel-HochNiedrig (wenn Sie TS kennen)
Tooling-ReifeHervorragendSehr gutGut
SprachunterstuetzungBeliebigBeliebigNur TypeScript
EchtzeitWebSockets/SSE (separat)SubscriptionsSubscriptions
Datei-UploadsNativWorkaround erforderlichWorkaround erforderlich
Oeffentliche APIsIdealGutNicht geeignet
CodegenerierungOptional (OpenAPI)Oft erforderlichNicht noetig
LeistungGutGut (mit Batching)Hervorragend (minimaler Overhead)
API-DokumentationSwagger/OpenAPISchema-IntrospectionTypeScript-Typen

Wann welchen Ansatz waehlen

Waehlen Sie REST, wenn

  • Sie eine oeffentliche API erstellen, die von Dritten in verschiedenen Sprachen konsumiert wird.
  • Sie HTTP-Caching auf CDN- oder Browser-Ebene benoetigen (z. B. eine inhaltsreiche Website).
  • Ihr Team Entwickler umfasst, die mit traditionellen HTTP APIs vertrauter sind.
  • Sie Microservices erstellen, die ueber verschiedene Sprachlaufzeiten hinweg miteinander kommunizieren.
  • Sie die einfachstmoegliche Architektur fuer eine unkomplizierte CRUD-Anwendung wuenschen.

Waehlen Sie GraphQL, wenn

  • Sie mehrere Clients (Web, Mobile, TV, Watch) haben, die unterschiedliche Datenformen von derselben API benoetigen.
  • Ihre Daten tief relational sind und Clients haeufig verschachtelte Daten in einer einzigen Anfrage benoetigen.
  • Sie Echtzeit-Features (Subscriptions) benoetigen, die eng mit Ihrer Abfrageschicht integriert sind.
  • Sie eine selbstdokumentierende API mit Introspection fuer Entwickler-Tooling wuenschen.
  • Sie ein Gateway erstellen, das Daten aus mehreren Backend-Diensten aggregiert.

Waehlen Sie tRPC, wenn

  • Ihr gesamter Stack TypeScript ist (sowohl Frontend als auch Backend).
  • Sie in einem Monorepo arbeiten, in dem Client- und Server-Code zusammenleben.
  • Sie Entwicklergeschwindigkeit schaetzen und die schnellstmoegliche Iterationsgeschwindigkeit wuenschen.
  • Sie ein internes Tool oder SaaS-Produkt erstellen, bei dem die API nicht oeffentlich exponiert wird.
  • Typsicherheit hoechste Prioritaet hat und Sie API-Diskrepanzen zur Kompilierzeit erkennen moechten.

Architekturmuster aus der Praxis

Hybrider Ansatz: tRPC + REST

Viele Teams verwenden tRPC fuer ihre interne Frontend-zu-Backend-Kommunikation und stellen eine separate REST API fuer Drittanbieter-Integrationen bereit:

// Interner tRPC-Router fuer das Dashboard
export const appRouter = router({
  user: userRouter,
  billing: billingRouter,
  analytics: analyticsRouter,
});

// Oeffentliche REST API fuer Integrationen
app.get('/api/v1/users/:id', async (req, res) => {
  const user = await db.users.findUnique({
    where: { id: req.params.id },
    select: publicUserFields,
  });
  res.json(user);
});

GraphQL als Gateway

GraphQL eignet sich gut als API-Gateway, das Daten aus mehreren Microservices foederiert:

// Gateway-Schema, das mehrere Dienste kombiniert
const gatewaySchema = stitchSchemas({
  subschemas: [
    { schema: userServiceSchema, executor: userServiceExecutor },
    { schema: orderServiceSchema, executor: orderServiceExecutor },
    { schema: inventoryServiceSchema, executor: inventoryServiceExecutor },
  ],
});

REST mit OpenAPI fuer Typsicherheit

Wenn Sie sich fuer REST entscheiden, aber Typsicherheit wuenschen, schliesst OpenAPI-Codegenerierung die Luecke:

# openapi.yaml
paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
# Typisierten Client generieren
npx openapi-typescript-codegen --input openapi.yaml --output src/api
// Generierter Client mit voller Typsicherheit
import { UserService } from './api';

const user = await UserService.getUser({ id: '42' });
// user ist vollstaendig typisiert

Leistungsbetrachtungen

REST-Leistung

REST profitiert von HTTP/2-Multiplexing und Browser-Caching. Fuer leseinteinsive APIs, die cachebare Inhalte bereitstellen, kann REST extrem schnell sein. Der Nachteil sind mehrere Roundtrips, wenn Sie Daten von verschiedenen Endpoints benoetigen.

GraphQL-Leistung

GraphQL reduziert Roundtrips, indem komplexe Abfragen in einer einzigen Anfrage ermoeglicht werden. Allerdings muss der Server potenziell komplexe Abfragebaeume aufloesen. Ohne richtige Optimierung (Data Loader, Abfragekomplexitaetslimits, Persisted Queries) kann GraphQL tatsaechlich langsamer als REST sein.

tRPC-Leistung

tRPC hat minimalen Serialisierungs-Overhead, da es einfaches JSON ueber HTTP kommuniziert. Mit aktiviertem Request-Batching werden mehrere Prozeduraufrufe in einer einzigen HTTP-Anfrage zusammengefasst, was Roundtrips reduziert. Die React Query-Integration bietet clientseitiges Caching, das Netzwerkanfragen weiter reduziert.

Fazit

Es gibt keinen einzelnen besten API-Ansatz. Die richtige Wahl haengt von Ihrem Team, Ihrem Technologie-Stack und Ihren Projektanforderungen ab.

REST bleibt die Standardwahl fuer oeffentliche APIs, Microservice-Kommunikation und Teams, die mit mehreren Sprachen arbeiten. Seine Einfachheit und universelle Unterstuetzung machen es zur sicheren Wahl fuer die meisten Szenarien.

GraphQL glaenzt, wenn Sie komplexe, relationale Daten und mehrere Client-Anwendungen haben, die unterschiedliche Sichten auf diese Daten benoetigen. Es fuegt Komplexitaet hinzu, aber die gebotene Flexibilitaet kann fuer den richtigen Anwendungsfall lohnend sein.

tRPC ist die beste Option fuer TypeScript-Monorepo-Projekte, bei denen Entwicklergeschwindigkeit und Typsicherheit Prioritaet haben. Es eliminiert eine ganze Kategorie von Bugs (API-Vertragsdiskrepanzen) und bietet die schnellste Entwicklungserfahrung der drei Ansaetze.

Viele erfolgreiche Teams verwenden eine Kombination dieser Ansaetze und setzen jeden dort ein, wo er am meisten Sinn ergibt. Das Wichtigste ist, die Kompromisse zu verstehen und eine bewusste Entscheidung zu treffen, anstatt einfach das zu verwenden, was Ihr letztes Projekt genutzt hat.

Verwandte BeitrÀge