die Datenschicht für React Apps richtig wählen
Die Wahl der Datenschicht entscheidet über Type-Safety, Caching-Verhalten und die Geschwindigkeit, mit der ein React-Team neue Features ausliefert. REST bleibt der pragmatische Standard, GraphQL bringt flexible Queries für komplexe Datenmodelle, und tRPC liefert End-to-End-Typsicherheit ohne Schema-Sprache, sofern Backend und Frontend im selben TypeScript-Monorepo liegen.
Inhaltsverzeichnis
- 1. Warum die Datenschicht-Entscheidung architektonische Folgen hat
- 2. REST: Grundprinzipien, Stärken und Grenzen
- 3. GraphQL: ein Endpoint, flexible Queries, eigene Komplexität
- 4. tRPC: End-to-End-Typsicherheit ohne Schema-Sprache
- 5. Type-Safety im Vergleich: Codegen vs. Inference
- 6. Caching-Verhalten der drei Ansätze in React
- 7. Teamgröße, Monorepo und Backend-Sprache als Faktoren
- 8. Migrationspfade zwischen den drei Ansätzen
- 9. REST, GraphQL und tRPC im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum die Datenschicht-Entscheidung architektonische Folgen hat
Die Entscheidung für die Datenschicht einer React-App fällt meist früh im Projekt und ist danach nur mit erheblichem Aufwand rückgängig zu machen. REST, GraphQL und tRPC unterscheiden sich nicht nur in der Syntax, sondern in grundlegenden Annahmen darüber, wie Frontend und Backend gekoppelt sind, wie Type-Safety entsteht und wie Caching funktioniert. Eine falsch gewählte Datenschicht zeigt sich selten sofort, sondern erst, wenn das Team wächst oder die Anwendung komplexere Datenbeziehungen abbilden muss.
Dieser Artikel vergleicht die drei Ansätze entlang der Kriterien, die in der Praxis tatsächlich den Ausschlag geben: Type-Safety, Caching-Verhalten, Teamstruktur und Migrationsaufwand. Ziel ist nicht, eine einzelne Datenschicht pauschal zu empfehlen, sondern die Kompromisse sichtbar zu machen, die jede Wahl mit sich bringt, damit die Entscheidung bewusst und nicht aus Gewohnheit getroffen wird.
Auffällig ist außerdem, dass die drei Ansätze unterschiedliche Fehlerklassen erzeugen, wenn die Datenschicht falsch gewählt wurde. Eine zu starre REST-API zeigt sich in immer neuen Spezial-Endpoints. Eine unnötig komplexe GraphQL-Schicht zeigt sich in langsamen Resolvern und N-plus-1-Problemen. Ein zu forsch eingeführtes tRPC zeigt sich, sobald ein zweites Frontend in einer anderen Sprache dazukommt und plötzlich komplett außen vor bleibt. Diese Symptome helfen im Nachhinein, eine falsche Entscheidung zu erkennen, bevor sie richtig teuer wird.
2. REST: Grundprinzipien, Stärken und Grenzen
REST bleibt die am weitesten verbreitete Datenschicht für React-Anwendungen, weil sie auf HTTP-Semantik aufbaut, die praktisch jedes Team bereits kennt: Ressourcen über URLs, Verben wie GET und POST, Statuscodes für Fehlerbehandlung. Diese Einfachheit macht REST zur naheliegenden Wahl für Teams mit gemischten Backend-Sprachen oder wenn die API auch von mobilen Clients oder Drittanbietern konsumiert wird, die kein GraphQL oder tRPC sprechen.
Die Grenzen von REST als Datenschicht zeigen sich bei verschachtelten Datenmodellen: Eine Übersichtsseite, die Beiträge, Autoren und Kommentarzahlen gleichzeitig braucht, erfordert entweder mehrere Roundtrips oder einen speziell zugeschnittenen Endpoint, der schnell zu einem unübersichtlichen Sammelbecken wird. Over-Fetching und Under-Fetching sind die typischen Symptome: Entweder liefert ein Endpoint mehr Felder als eine Komponente braucht, oder es fehlt genau das eine Feld, das eine zweite Anfrage nötig macht.
Ein bewährter Mittelweg innerhalb einer REST-Datenschicht ist ein Backend-for-Frontend, kurz BFF: ein schlanker Aggregations-Layer, der mehrere interne REST-Aufrufe bündelt und dem Frontend einen einzigen, auf die jeweilige Seite zugeschnittenen Endpoint anbietet. Das behält die Einfachheit von REST bei, verlagert die Aggregationslogik aber vom Client ins Backend, wo sie näher an den Datenquellen liegt und weniger Roundtrips über eine potenziell langsame Mobilfunkverbindung erzeugt.
// api-client.ts — Typed REST client generated from an OpenAPI spec
import createClient from "openapi-fetch";
import type { paths } from "./generated/openapi-types";
const client = createClient<paths>({ baseUrl: "https://api.mironsoft.de" });
// Type-safe REST call: path, method and response shape are all inferred
async function fetchProduct(id: string) {
const { data, error } = await client.GET("/products/{id}", {
params: { path: { id } },
});
if (error) throw new Error("Product fetch failed");
return data; // fully typed from the OpenAPI schema
}
Ein oft unterschätzter Vorteil von REST als Datenschicht ist die HTTP-native Cache-Semantik: Header wie ETag, Cache-Control und Last-Modified funktionieren ohne zusätzliche Bibliothek und lassen sich von CDNs, Reverse Proxies und Browsern gleichermaßen auswerten. Ein GET-Request auf einen unveränderten Produkt-Endpoint kann so komplett am Anwendungsserver vorbei aus einem Edge-Cache beantwortet werden, was bei GraphQL wegen des einzelnen POST-Endpoints deutlich schwieriger umzusetzen ist. Für öffentliche, stark cachebare Inhalte bleibt dieser Vorteil von REST als Datenschicht praktisch relevant, unabhängig von der Type-Safety-Diskussion.
3. GraphQL: ein Endpoint, flexible Queries, eigene Komplexität
GraphQL löst das Over-Fetching-Problem von REST, indem Clients exakt die Felder anfragen, die eine Komponente tatsächlich braucht, über einen einzigen Endpoint hinweg. Für React-Anwendungen mit stark verschachtelten Datenmodellen, etwa E-Commerce-Kataloge mit Varianten, Preisen und Lagerbestand, ist diese Flexibilität ein erheblicher Vorteil gegenüber einer starren REST-Datenschicht. Ein Client kann eine Query formulieren, die genau einer Komponente entspricht, statt mehrere REST-Endpoints zu orchestrieren.
Die Kehrseite ist zusätzliche Komplexität auf Backend-Seite: Ein GraphQL-Server braucht Resolver, Schema-Definitionen und meist eine Lösung für das N-plus-1-Problem wie DataLoader. Für kleine Teams ohne dedizierte Backend-Expertise kann diese Datenschicht mehr Aufwand erzeugen, als sie an Frontend-Flexibilität einspart. GraphQL lohnt sich vor allem dann, wenn mehrere Client-Typen, etwa Web und Mobile, dieselbe API mit unterschiedlichen Datenbedürfnissen konsumieren.
# product-detail.graphql — One query replaces three REST roundtrips
query ProductDetail($id: ID!) {
product(id: $id) {
name
priceCents
variants {
sku
stockLevel
}
reviews(first: 5) {
rating
author {
displayName
}
}
}
}
4. tRPC: End-to-End-Typsicherheit ohne Schema-Sprache
tRPC verfolgt einen fundamental anderen Ansatz für die Datenschicht: Statt eine separate Schema-Sprache wie SDL zu definieren, wird der Backend-Router direkt als TypeScript-Code geschrieben, und der Frontend-Client leitet die Typen über TypeScripts Inference-Mechanismus direkt aus diesem Router ab. Es gibt keinen Codegen-Schritt, keine .graphql-Dateien und keinen separaten Build-Schritt für Typen, weil Backend und Frontend denselben TypeScript-Compiler nutzen.
Diese Eleganz hat eine harte Voraussetzung: tRPC funktioniert nur, wenn Backend und Frontend im selben Monorepo liegen und beide in TypeScript geschrieben sind. Für Teams mit einem Java- oder PHP-Backend ist tRPC keine Option. Für TypeScript-Vollstack-Teams in einem Monorepo ist tRPC dagegen oft die Datenschicht mit dem geringsten Wartungsaufwand, weil ein falscher Funktionsaufruf bereits beim Kompilieren auffällt, nicht erst zur Laufzeit im Browser.
Ein weiterer, oft unterschätzter Vorteil: Refactorings, die eine Prozedur umbenennen oder ein Eingabefeld entfernen, werden vom TypeScript-Compiler sofort an jeder Aufrufstelle im Frontend als Fehler markiert. Bei REST oder GraphQL bleibt eine vergessene Aufrufstelle oft bis zur Laufzeit unentdeckt, weil Compiler und API-Vertrag getrennt geprüft werden. Diese sofortige Rückmeldung macht große Refactorings in einer tRPC-Datenschicht spürbar risikoärmer als in den beiden anderen Ansätzen.
// server/router.ts — tRPC router, plain TypeScript, no schema language
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
export const appRouter = router({
product: {
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
return ctx.db.product.findUniqueOrThrow({ where: { id: input.id } });
}),
},
});
export type AppRouter = typeof appRouter;
// client.ts — Types are inferred directly from AppRouter, no codegen step
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../server/router";
export const trpc = createTRPCReact<AppRouter>();
// Component usage: fully typed, autocompletion for every field
function ProductPage({ id }: { id: string }) {
const { data } = trpc.product.byId.useQuery({ id });
return data ? <h1>{data.name}</h1> : null;
}
Ein weiterer praktischer Aspekt von tRPC als Datenschicht ist die Integration mit Middleware und Kontext: Authentifizierung, Rate-Limiting und Logging lassen sich als wiederverwendbare Middleware-Funktionen direkt im Router definieren, ähnlich wie bei Express oder Fastify, aber mit vollständiger Typweitergabe an nachgelagerte Prozeduren. Ein protectedProcedure, das den authentifizierten Nutzer bereits typisiert in den Kontext einfügt, erspart in jeder einzelnen Prozedur wiederholte Typprüfungen und macht Fehler bei fehlender Authentifizierung schon beim Kompilieren sichtbar, nicht erst beim Testen der Route.
5. Type-Safety im Vergleich: Codegen vs. Inference
Type-Safety ist der Bereich, in dem sich die drei Ansätze am deutlichsten unterscheiden. REST erreicht Type-Safety nur über zusätzliche Werkzeuge wie OpenAPI-Codegen, der Typen aus einer Spezifikation generiert. Diese Spezifikation muss manuell gepflegt oder aus Code-Annotationen erzeugt werden, was eine zusätzliche Fehlerquelle bedeutet: Weicht die generierte Spezifikation von der tatsächlichen API ab, meldet TypeScript trotzdem grünes Licht.
Ein häufig übersehener Punkt bei der Type-Safety-Diskussion ist Laufzeit-Validierung: Auch mit vollständig korrekten Typen auf beiden Seiten kann eine externe API abweichende Daten liefern, etwa nach einem unangekündigten Feld-Rename beim Drittanbieter. Bibliotheken wie Zod ergänzen deshalb sowohl REST- als auch tRPC-Datenschicht-Setups um Laufzeit-Validierung, die TypeScripts rein statische Prüfung zur Kompilierzeit sinnvoll ergänzt und echte Diskrepanzen zwischen erwarteten und tatsächlichen Daten sofort sichtbar macht.
GraphQL erreicht Type-Safety über codegen-Tools wie GraphQL Code Generator, die aus dem Schema und den Query-Dokumenten typisierte Hooks erzeugen. Der Codegen-Schritt läuft meist als Build-Task und muss bei jeder Schema-Änderung neu ausgeführt werden. tRPC benötigt keinen separaten Codegen-Schritt, weil die Typinferenz direkt vom TypeScript-Compiler übernommen wird. Diese Direktheit ist der Hauptgrund, warum viele TypeScript-Teams tRPC für interne Tools und Admin-Oberflächen bevorzugen, wo kein Drittanbieter die API konsumiert.
6. Caching-Verhalten der drei Ansätze in React
Beim Caching zeigt sich ein interessanter Unterschied zwischen den drei Datenschicht-Ansätzen. REST-Endpoints lassen sich mit TanStack Query pro URL cachen, was intuitiv, aber ohne Normalisierung ist: Dasselbe Produkt, das über zwei unterschiedliche Endpoints geladen wird, existiert im Cache als zwei unabhängige Kopien. GraphQL mit Apollo Client oder Relay bringt eingebaute Normalisierung mit, bei der jedes Objekt unabhängig vom anfragenden Query-Dokument konsistent gehalten wird.
Diese Caching-Unterschiede wirken sich unmittelbar auf die gefühlte Geschwindigkeit einer Anwendung aus, besonders bei Navigationswechseln zwischen Übersichts- und Detailansicht derselben Objekte.
Ein hilfreicher Zwischenweg für tRPC-Projekte, die dennoch von etwas mehr Konsistenz profitieren wollen, ist eine eigene, dünne Normalisierungsschicht oberhalb von TanStack Query, die bekannte Objekttypen anhand ihrer ID in einer zentralen Map hält und Query-Ergebnisse beim Empfang dorthin spiegelt. Das ist kein vollwertiger Ersatz für Apollos automatische Normalisierung, reduziert aber doppelte Datenhaltung in größeren tRPC-Datenschicht-Projekten spürbar, ohne GraphQL einzuführen.
tRPC nutzt in der Regel TanStack Query als Caching-Layer, mit demselben Query-Key-basierten Modell wie REST, also ohne automatische Normalisierung über Objekttypen hinweg. Für viele Anwendungen ist das ausreichend, weil TanStack Query mit gezielter Invalidierung über queryClient.invalidateQueries die meisten praktischen Fälle abdeckt. Wer echte Objektnormalisierung über mehrere Views hinweg braucht, findet diese nur bei GraphQL-Clients mit eingebautem Cache-Graphen, nicht bei REST oder tRPC in ihrer Standardkonfiguration.
Ein oft übersehener Aspekt beim Caching-Vergleich ist die Invalidierungsgranularität: Bei GraphQL mit Apollo genügt oft ein einzelner cache.modify-Aufruf auf ein Objekt, damit alle betroffenen Views konsistent bleiben, unabhängig davon, über welche Query sie das Objekt ursprünglich geladen haben. Bei REST und tRPC mit TanStack Query muss man dagegen explizit wissen, welche Query-Keys von einer Änderung betroffen sind, und diese gezielt mit invalidateQueries markieren, was bei komplexen Abhängigkeiten zwischen Query-Keys zu einer eigenen kleinen Buchhaltungsaufgabe im Code werden kann.
7. Teamgröße, Monorepo und Backend-Sprache als Faktoren
Die Wahl der Datenschicht hängt stärker von organisatorischen Faktoren ab, als viele technische Diskussionen vermuten lassen. Ein kleines Team mit TypeScript auf beiden Seiten in einem Monorepo profitiert am meisten von tRPC, weil Reibung zwischen Frontend und Backend praktisch verschwindet. Ein größeres Unternehmen mit mehreren Backend-Sprachen, mobilen Clients und externen API-Konsumenten fährt oft besser mit REST oder GraphQL, weil beide sprachunabhängig funktionieren.
Auch die Onboarding-Geschwindigkeit neuer Teammitglieder unterscheidet sich merklich zwischen den drei Ansätzen. Eine REST-Datenschicht mit klarer OpenAPI-Dokumentation ist für neue Entwicklerinnen meist am schnellsten zu verstehen, weil das Konzept aus nahezu jeder Web-Erfahrung bekannt ist. GraphQL verlangt zusätzliches Verständnis für Schema, Resolver und Query-Sprache, während tRPC kaum Einarbeitung braucht, sofern die neue Person bereits mit TypeScript vertraut ist, weil der Router einfach normaler, lesbarer Anwendungscode ist.
GraphQL lohnt sich besonders, wenn mehrere Frontend-Teams dieselbe API mit unterschiedlichen Datenbedürfnissen nutzen, etwa ein Web-Team und ein Mobile-Team, die jeweils unterschiedliche Felder derselben Objekte brauchen. In diesem Szenario verhindert GraphQL, dass das Backend-Team für jeden Client einen eigenen REST-Endpoint pflegen muss. Für ein einzelnes React-Team ohne diese Anforderung ist der zusätzliche Aufwand einer GraphQL-Datenschicht oft nicht gerechtfertigt.
8. Migrationspfade zwischen den drei Ansätzen
Ein kompletter Wechsel der Datenschicht in einer laufenden Anwendung ist selten in einem Schritt sinnvoll. Der übliche Weg von REST zu GraphQL führt über einen GraphQL-Server, der bestehende REST-Endpoints als Resolver umschließt, sogenanntes Schema Stitching oder ein BFF-Layer, sodass Frontend-Teams schrittweise auf GraphQL-Queries umstellen können, während das Backend unverändert bleibt. Ein direkter Big-Bang-Wechsel ohne Übergangsphase führt fast immer zu Verzögerungen, weil unterschätzt wird, wie viele implizite Annahmen im bestehenden REST-Code stecken.
Auch bei einer Migration von GraphQL zu tRPC, etwa weil ein Team feststellt, dass der Resolver-Aufwand den Nutzen nicht rechtfertigt, lohnt sich derselbe schrittweise Ansatz: Neue, intern genutzte Admin-Bereiche werden über tRPC-Prozeduren implementiert, während die öffentliche, von mehreren Clients konsumierte API auf GraphQL verbleibt. Diese hybride Datenschicht mit zwei parallelen Zugriffswegen wirkt zunächst unelegant, ist in der Praxis aber oft die pragmatischste Lösung, weil sie jeden Anwendungsfall mit dem dafür passenden Werkzeug bedient, statt eine einzige Technologie um jeden Preis durchzusetzen.
Ein Wechsel zu tRPC ist nur sinnvoll, wenn das Backend ohnehin in TypeScript vorliegt oder migriert wird. In diesem Fall lässt sich tRPC parallel zu bestehenden REST-Endpoints betreiben, wobei neue Features direkt über tRPC-Prozeduren implementiert werden, während alte Endpoints auslaufen. Diese schrittweise Koexistenz ist deutlich risikoärmer als eine komplette Neuimplementierung der Datenschicht und erlaubt es, den Nutzen früh im Projekt zu validieren.
Unabhängig von der Zielarchitektur gilt: Ein Big-Bang-Wechsel der Datenschicht ohne Feature-Flags oder eine Übergangsphase bindet unverhältnismäßig viel Teamkapazität und blockiert oft parallele Feature-Entwicklung für Wochen. Erfolgreiche Migrationen laufen deshalb fast immer inkrementell, Endpoint für Endpoint oder View für View, mit klaren Metriken, die belegen, dass die neue Datenschicht tatsächlich den erhofften Vorteil bei Type-Safety oder Caching liefert, bevor der nächste Abschnitt der Anwendung migriert wird.
9. REST, GraphQL und tRPC im direkten Vergleich
Die folgende Tabelle fasst die entscheidenden Unterschiede zusammen und dient als schnelle Referenz für die Entscheidungsfindung im Team. Sie ersetzt keine individuelle Analyse, macht aber die wichtigsten Kompromisse auf einen Blick sichtbar, bevor eine Entscheidung über die Datenschicht getroffen wird.
Wer unsicher ist, sollte klein anfangen: ein einzelnes Feature testweise über die favorisierte Datenschicht umsetzen, Team-Feedback nach zwei bis drei Sprints einholen und erst danach die Entscheidung auf weitere Bereiche der Anwendung ausweiten.
| Kriterium | REST | GraphQL | tRPC |
|---|---|---|---|
| Type-Safety | Über OpenAPI-Codegen | Über Schema + Codegen | Nativ per Inference |
| Backend-Sprache | Beliebig | Beliebig | Nur TypeScript |
| Caching-Normalisierung | Keine, per URL | Objektbasiert | Keine, per Query-Key |
| Over/Under-Fetching | Häufiges Problem | Gelöst durch Queries | Prozedurweise, gering |
| Externe Konsumenten | Sehr gut geeignet | Geeignet, mehr Aufwand | Ungeeignet |
| Setup-Aufwand | Gering | Hoch | Gering im Monorepo |
Ein Aspekt, der in der Tabelle nicht direkt sichtbar wird, ist die Fehlerbehandlung an der Schnittstelle: REST kommuniziert Fehler über HTTP-Statuscodes, was von jedem Client, jeder Firewall und jedem Monitoring-Tool ohne Zusatzwissen verstanden wird. GraphQL gibt bei Teilfehlern oft einen HTTP-200-Status mit einem separaten errors-Array zurück, was bestehende HTTP-Monitoring-Tools ohne GraphQL-spezifische Anpassung leicht übersieht. tRPC nutzt eigene, typisierte Fehlerklassen, die auf Client-Seite exakt behandelt werden können, aber ebenfalls eine gewisse Einarbeitung erfordern, bevor Monitoring und Alerting korrekt funktionieren.
Keine dieser drei Datenschicht-Optionen ist universell überlegen. REST bleibt die richtige Wahl für offene APIs und gemischte Backend-Landschaften, GraphQL für komplexe Datenmodelle mit mehreren Client-Typen, und tRPC für TypeScript-Vollstack-Teams, die maximale Type-Safety mit minimalem Setup wollen. Die Tabelle macht sichtbar, dass die Entscheidung weniger eine Technologiefrage als eine Frage der Teamstruktur ist.
Mironsoft
Architekturberatung für REST-, GraphQL- und tRPC-Datenschichten
Unsicher, welche Datenschicht zu eurem Team passt?
Wir analysieren Teamstruktur, Backend-Landschaft und Datenmodell und empfehlen eine Datenschicht, die zur tatsächlichen Situation passt, nicht zum aktuellen Hype.
Architektur-Review
Bewertung der bestehenden REST- oder GraphQL-API auf Type-Safety und Caching
tRPC-Einführung
Setup in TypeScript-Monorepos mit TanStack Query Integration
Migration
Schrittweiser Umstieg über BFF-Layer ohne Big-Bang-Risiko
10. Zusammenfassung
Die Wahl der Datenschicht für eine React-App ist keine reine Geschmacksfrage, sondern hängt von Teamstruktur, Backend-Sprache und der Komplexität des Datenmodells ab. REST bleibt die pragmatische Standardwahl für offene APIs und gemischte Sprach-Landschaften, verlangt aber zusätzliche Werkzeuge für Type-Safety. GraphQL löst Over-Fetching durch flexible Queries, bringt aber Resolver-Komplexität und einen Codegen-Schritt mit sich.
tRPC liefert die direkteste Type-Safety über TypeScript-Inference ohne Schema-Sprache, ist aber strikt an ein TypeScript-Monorepo gebunden und für externe API-Konsumenten ungeeignet. Beim Caching bietet nur GraphQL echte Objektnormalisierung über Views hinweg, während REST und tRPC auf Query-Key-basiertes Caching mit TanStack Query setzen. Die richtige Datenschicht ist die, die zur tatsächlichen Teamstruktur passt, nicht die technisch aufwendigste Option.
Wer diese Entscheidung dokumentiert, etwa als kurzes Architecture Decision Record, erspart dem Team spätere Grundsatzdiskussionen, sobald neue Mitglieder dazustoßen und dieselbe Frage erneut aufwerfen.
REST vs. GraphQL vs. tRPC — Das Wichtigste auf einen Blick
REST
Pragmatisch, sprachunabhängig, aber anfällig für Over- und Under-Fetching ohne dedizierte Endpoints.
GraphQL
Flexible Queries über einen Endpoint, objektbasierte Cache-Normalisierung, höherer Backend-Aufwand.
tRPC
Native Type-Safety ohne Codegen, aber nur im TypeScript-Monorepo einsetzbar.
Entscheidung
Teamstruktur und Backend-Sprache entscheiden, nicht technische Präferenz allein.