APIs ohne Schema und ohne Codegenerierung
Statische Typen enden normalerweise an der Netzwerkgrenze: Der Client weiss nicht, was der Server zurueckgibt, ausser er verlaesst sich auf manuell gepflegte Interfaces oder generierte Client-Codes. tRPC schliesst diese Luecke, indem Router-Typen direkt vom Server importiert werden, ganz ohne REST-Konventionen, ohne GraphQL-Schema und ohne separaten Codegenerierungs-Schritt.
Inhaltsverzeichnis
- 1. Das Grundproblem: Typsicherheit endet an der Netzwerkgrenze
- 2. Router und Procedures definieren
- 3. Input-Validierung mit Zod in Procedures
- 4. Der Client: Typen importieren ohne Codegenerierung
- 5. Queries und Mutations mit React Query kombinieren
- 6. Middleware und Context in tRPC
- 7. Fehlerbehandlung mit TRPCError
- 8. Grenzen von tRPC: wann REST oder GraphQL besser passen
- 9. tRPC im Vergleich zu REST und GraphQL
- 10. Zusammenfassung
- 11. FAQ
1. Das Grundproblem: Typsicherheit endet an der Netzwerkgrenze
Ein typisches Fullstack-TypeScript-Projekt hat auf dem Server vollstaendig typisierte Funktionen und im Frontend vollstaendig typisierte Komponenten, aber dazwischen liegt eine Netzwerkgrenze, an der die Typsicherheit meist endet. Ein REST-Endpunkt gibt JSON zurueck, dessen Struktur der Client nur durch ein manuell gepflegtes Interface kennt, das bei jeder Aenderung am Server von Hand nachgezogen werden muss. Genau dieses Problem loest tRPC: Es ermoeglicht Ende-zu-Ende-Typsicherheit, indem der Typ des Server-Routers direkt in den Client importiert wird.
Der zentrale Unterschied zu REST und GraphQL: tRPC braucht kein separates Schema und keinen Codegenerierungs-Schritt. Der Router selbst ist die Typ-Quelle, TypeScript liest die Typinformation direkt aus dem exportierten Router-Typ, ohne dass ein Build-Schritt zwischengeschaltet werden muss. Das funktioniert allerdings nur in einem Monorepo oder Projekt-Setup, in dem Client und Server auf denselben TypeScript-Typ zugreifen koennen, etwa durch ein gemeinsames Paket oder Workspace.
Fuer Teams, die bereits vollstaendig in TypeScript arbeiten und keine ausserhalb liegenden Konsumenten wie mobile Apps in anderen Sprachen bedienen muessen, bietet tRPC die direkteste Form von Ende-zu-Ende-Typsicherheit im aktuellen Oekosystem. Dieser Artikel zeigt, wie Router, Validierung und Client-Integration zusammenspielen, und wo die Grenzen dieses Ansatzes liegen.
2. Router und Procedures definieren
Ein tRPC-Router besteht aus einer Sammlung von Procedures, die jeweils entweder eine Query fuer lesende Operationen oder eine Mutation fuer schreibende Operationen darstellen. Jede Procedure wird ueber einen Procedure-Builder definiert, der optional Input-Validierung, Middleware und schliesslich einen Resolver verkettet. Der Rueckgabetyp des Resolvers wird von TypeScript automatisch inferiert und muss nirgendwo manuell wiederholt werden.
Router lassen sich verschachteln, sodass grosse Anwendungen ihre Procedures in logische Gruppen unterteilen koennen, etwa einen userRouter und einen postRouter, die anschliessend zu einem appRouter zusammengefuehrt werden. Genau dieser appRouter-Typ ist es, der spaeter in den Client importiert wird und dort die gesamte API-Oberflaeche typsicher beschreibt.
// server/trpc.ts — base setup
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
// server/routers/post.ts
import { z } from "zod";
import { publicProcedure, router } from "../trpc";
export const postRouter = router({
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
// input.id is typed as string, inferred from the Zod schema
return { id: input.id, title: "Example Post" };
}),
create: publicProcedure
.input(z.object({ title: z.string().min(3) }))
.mutation(async ({ input }) => {
return { id: "p_1", title: input.title };
}),
});
// server/routers/_app.ts
import { router } from "../trpc";
import { postRouter } from "./post";
export const appRouter = router({
post: postRouter,
});
// This type is the only thing the client needs to import
export type AppRouter = typeof appRouter;
3. Input-Validierung mit Zod in Procedures
Der Aufruf .input(schema) bindet ein Zod-Schema an eine Procedure, das eingehende Daten zur Laufzeit validiert, bevor der Resolver ausgefuehrt wird. Der aus dem Schema abgeleitete Typ steht im Resolver automatisch unter input zur Verfuegung, ohne dass eine zusaetzliche Typannotation noetig waere. Fuer Ende-zu-Ende-Typsicherheit bedeutet das, dass Client und Server exakt dieselbe Vorstellung davon haben, welche Felder eine Anfrage enthalten muss, weil beide auf denselben Router-Typ zugreifen.
Zod ist zwar die haeufigste Wahl fuer tRPC, aber nicht die einzige. Jede Bibliothek, die dem Standard-Schema-Interface folgt, etwa Valibot oder ArkType, laesst sich ebenso einbinden. Wichtig ist nur, dass die Bibliothek sowohl Laufzeit-Validierung als auch eine Methode zur statischen Typableitung bereitstellt, damit Input-Typ und Validierungslogik nicht auseinanderlaufen koennen.
4. Der Client: Typen importieren ohne Codegenerierung
Der tRPC-Client wird mit createTRPCProxyClient oder, im React-Kontext, mit createTRPCReact erzeugt und erhaelt als generischen Typ-Parameter ausschliesslich den AppRouter-Typ vom Server, nicht den eigentlichen Server-Code. Diese Trennung ist entscheidend: Es wird nur der Typ importiert, zur Laufzeit findet kein direkter Aufruf von Server-Funktionen statt, die Kommunikation laeuft weiterhin ueber HTTP.
Der praktische Effekt fuer TypeScript-Entwickler: client.post.byId.query({ id: "1" }) wird vollstaendig typgeprueft, inklusive Autovervollstaendigung fuer verfuegbare Router-Pfade und erwarteter Eingabeform. Aendert sich der Router auf dem Server, etwa durch Umbenennen einer Procedure, meldet der Compiler auf der Client-Seite sofort einen Fehler an jeder Stelle, die den alten Namen noch verwendet.
// client/trpc.ts — imports only the type, not server code
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "../server/routers/_app";
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: "https://api.example.com/trpc",
}),
],
});
// Fully typed call, autocomplete works for router path and input shape
const post = await trpc.post.byId.query({ id: "1" });
console.log(post.title); // typed as string, no manual interface needed
5. Queries und Mutations mit React Query kombinieren
Fuer React-Anwendungen bietet tRPC eine offizielle Integration mit TanStack Query, die aus jeder Query-Procedure automatisch einen typisierten useQuery-Hook und aus jeder Mutation-Procedure einen typisierten useMutation-Hook erzeugt. Caching, Refetching und optimistische Updates funktionieren dabei exakt wie bei manuell konfiguriertem React Query, nur dass Query-Key, Input und Rueckgabetyp automatisch aus dem Router abgeleitet werden.
Dieser Ansatz reduziert den ueblichen Boilerplate erheblich: Statt fuer jeden Endpunkt einen eigenen Query-Key und ein eigenes Response-Interface zu pflegen, reicht der Aufruf eines generierten Hooks wie trpc.post.byId.useQuery({ id }), der Caching-Verhalten, Ladezustand und Typsicherheit automatisch mitbringt. Fuer TypeScript-Teams mit React Query im Bestand ist dies meist der ueberzeugendste Einstiegspunkt fuer tRPC.
6. Middleware und Context in tRPC
Der tRPC-Context wird pro Request einmal erzeugt und an alle Procedures weitergereicht, etwa mit einer Datenbankverbindung oder Informationen zum authentifizierten Benutzer. Middleware kann diesen Context erweitern, bevor er den Resolver erreicht, und dabei den Typ des Contexts fuer nachfolgende Procedures gezielt verschmaelern oder erweitern. Eine typische Middleware prueft die Authentifizierung und erweitert den Context um ein garantiert vorhandenes user-Feld.
Dieses Muster erlaubt es, geschuetzte Procedures ueber einen eigenen Procedure-Builder zu definieren, der die Auth-Middleware bereits enthaelt. Ein Resolver, der auf diesem geschuetzten Builder aufbaut, kann sich vollstaendig darauf verlassen, dass ctx.user existiert, ohne selbst noch eine Null-Pruefung durchzufuehren, weil TypeScript den erweiterten Context-Typ bereits kennt.
// server/trpc.ts — auth middleware narrows the context type
import { initTRPC, TRPCError } from "@trpc/server";
interface Context {
user: { id: string; role: string } | null;
}
const t = initTRPC.context<Context>().create();
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// ctx.user is now guaranteed non-null for downstream procedures
user: ctx.user,
},
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
// server/routers/profile.ts
export const profileRouter = router({
me: protectedProcedure.query(({ ctx }) => {
// No null check needed — the type system already knows ctx.user exists
return { id: ctx.user.id, role: ctx.user.role };
}),
});
7. Fehlerbehandlung mit TRPCError
tRPC definiert eine feste Menge an Fehlercodes wie UNAUTHORIZED, NOT_FOUND oder BAD_REQUEST, die ueber die Klasse TRPCError geworfen werden. Jeder Code wird automatisch auf einen passenden HTTP-Statuscode abgebildet, ohne dass Entwickler diese Zuordnung selbst pflegen muessen. Auf der Client-Seite lassen sich diese Fehler typsicher unterscheiden, weil TRPCClientError den Code als Teil seines Typs mitfuehrt.
Ein besonderer Vorteil fuer TypeScript-Projekte: Validierungsfehler aus dem Zod-Schema werden automatisch in eine strukturierte TRPCError mit Code BAD_REQUEST und den urspruenglichen Zod-Issues im cause-Feld umgewandelt. Damit muss die Input-Validierung nicht manuell in einen eigenen Fehlerpfad uebersetzt werden, das Framework uebernimmt diese Zuordnung automatisch fuer jede Procedure.
8. Grenzen von tRPC: wann REST oder GraphQL besser passen
tRPC setzt voraus, dass Client und Server in TypeScript geschrieben sind und denselben Router-Typ importieren koennen. Sobald ein externer Konsument beteiligt ist, etwa eine mobile App in Kotlin oder Swift, ein Partnerunternehmen mit eigenem Backend, oder eine oeffentliche API mit unbekannten Konsumenten, funktioniert der Typ-Import naturgemaess nicht mehr. In solchen Faellen bleibt REST mit OpenAPI oder GraphQL mit seinem sprachagnostischen Schema die passendere Wahl.
Auch bei sehr grossen, dezentral entwickelten Systemen mit vielen unabhaengigen Teams kann die enge Kopplung zwischen Client und Server-Typ zum Problem werden, weil jede Aenderung am Router theoretisch alle Konsumenten des Typs betrifft. GraphQL entkoppelt Client und Server bewusst ueber ein explizites Schema, was in solchen Organisationsformen mehr Stabilitaet bietet als die direkte Typkopplung von tRPC.
| Kriterium | tRPC | REST | GraphQL |
|---|---|---|---|
| Typsicherheit ohne Codegen | Ja, direkter Typ-Import | Nein, manuell oder via OpenAPI-Codegen | Nein, via GraphQL-Codegen |
| Sprachunabhaengige Konsumenten | Nicht vorgesehen | Ja, universell | Ja, ueber Schema |
| Setup-Aufwand | Niedrig, kein Schema noetig | Niedrig bis mittel | Hoch, Schema und Resolver |
| Oeffentliche API-Dokumentation | Kein eingebautes Schema | Ueber OpenAPI moeglich | Eingebautes, introspektierbares Schema |
9. tRPC im Vergleich zu REST und GraphQL
Der Vergleich zeigt deutlich: tRPC optimiert konsequent fuer Entwicklergeschwindigkeit innerhalb eines TypeScript-Monorepos, waehrend REST und GraphQL fuer Interoperabilitaet ueber Sprach- und Organisationsgrenzen hinweg optimieren. Fuer ein internes Dashboard, das von einem einzigen Frontend-Team konsumiert wird, ist tRPC oft die schnellste Loesung mit der geringsten Menge an Boilerplate. Fuer eine oeffentliche API mit Drittanbieter-Integrationen ist tRPC hingegen die falsche Wahl.
Manche Teams kombinieren beide Ansaetze bewusst: interne Admin-Tools und Dashboards laufen ueber tRPC fuer maximale Entwicklergeschwindigkeit, waehrend die oeffentliche, extern konsumierte API ueber REST oder GraphQL bereitgestellt wird. Diese Kombination nutzt die Staerken beider Ansaetze, ohne die Schwaechen des jeweils anderen in Kauf nehmen zu muessen.
Mironsoft
TypeScript-Fullstack, API-Architektur und Ende-zu-Ende-Typsicherheit
Frontend und Backend ohne durchgaengige Typsicherheit?
Wir bauen tRPC-Router mit Zod-Validierung, React-Query-Integration und geschuetzten Procedures fuer TypeScript-Monorepos, in denen Client und Server denselben Typ teilen.
Architektur-Bewertung
Einschaetzung, ob tRPC oder REST/GraphQL zu eurem Projekt passt
Router-Design
Saubere Procedure-Struktur mit Zod-Validierung und geschuetzten Routen
Frontend-Integration
React-Query-Hooks, Caching-Strategie und optimistische Updates
10. Zusammenfassung
tRPC loest ein spezifisches, aber haeufiges Problem: Ende-zu-Ende-Typsicherheit in TypeScript-Monorepos, ohne dass ein Schema oder ein separater Codegenerierungs-Schritt zwischengeschaltet werden muss. Router und Procedures definieren die gesamte API-Oberflaeche, Zod validiert Eingaben zur Laufzeit und leitet gleichzeitig den statischen Typ ab, und der Client importiert ausschliesslich den Router-Typ, nicht den Server-Code selbst. Middleware erweitert den Context typsicher, TRPCError liefert konsistente Fehlercodes ueber alle Procedures hinweg.
Die Grenzen von tRPC sind ebenso klar wie seine Staerken: sobald externe, nicht-TypeScript-Konsumenten ins Spiel kommen, ist REST oder GraphQL die passendere Wahl. Fuer interne Tools, Admin-Dashboards und Anwendungen mit einem einzigen Frontend-Team bleibt tRPC jedoch eine der effizientesten Moeglichkeiten, Typsicherheit tatsaechlich vom Server bis in die UI durchzureichen, ohne dabei Entwicklergeschwindigkeit zu opfern.
tRPC fuer Ende-zu-Ende-Typsicherheit: Das Wichtigste auf einen Blick
Router als Typ-Quelle
Der Client importiert ausschliesslich den AppRouter-Typ, keine separate Schema-Datei noetig.
Zod-Validierung
Ein Schema pro Procedure liefert Laufzeit-Validierung und Input-Typ gleichzeitig.
React-Query-Integration
Generierte Hooks bringen Caching und Typsicherheit ohne manuelle Query-Keys mit.
Grenzen
Nur fuer TypeScript-zu-TypeScript-Kommunikation, ungeeignet fuer sprachagnostische APIs.