
REST API vs GraphQL vs tRPC in 2026: Welches sollten Sie verwenden?
đ· Stanislav Kondratiev / PexelsREST 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.
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
| Feature | REST | GraphQL | tRPC |
|---|---|---|---|
| Typsicherheit | Manuell (OpenAPI Codegen) | Schema-basiert | Automatisch (TypeScript) |
| Over-Fetching | Haeufig | Eliminiert | Minimal (typisierte Returns) |
| Under-Fetching | Haeufig (N+1-Anfragen) | Eliminiert | Minimal |
| Caching | Hervorragend (HTTP-nativ) | Komplex (clientseitig) | React Query eingebaut |
| Lernkurve | Niedrig | Mittel-Hoch | Niedrig (wenn Sie TS kennen) |
| Tooling-Reife | Hervorragend | Sehr gut | Gut |
| Sprachunterstuetzung | Beliebig | Beliebig | Nur TypeScript |
| Echtzeit | WebSockets/SSE (separat) | Subscriptions | Subscriptions |
| Datei-Uploads | Nativ | Workaround erforderlich | Workaround erforderlich |
| Oeffentliche APIs | Ideal | Gut | Nicht geeignet |
| Codegenerierung | Optional (OpenAPI) | Oft erforderlich | Nicht noetig |
| Leistung | Gut | Gut (mit Batching) | Hervorragend (minimaler Overhead) |
| API-Dokumentation | Swagger/OpenAPI | Schema-Introspection | TypeScript-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.