tRPC fuer Ende-zu-Ende-Typsicherheit ohne REST und GraphQL
AI generated
<T>
type
TypeScript · tRPC · Fullstack · React Query
tRPC fuer Ende-zu-Ende-Typsicherheit
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.

18 Min. Lesezeit Router · Procedures · Zod · React Query tRPC 11.x · TypeScript 5.x

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.

11. FAQ: tRPC fuer Ende-zu-Ende-Typsicherheit

1Braucht tRPC ein Schema wie GraphQL?
Nein, der Router-Typ selbst dient als Schema, kein Codegenerierungs-Schritt noetig.
2Funktioniert tRPC ausserhalb eines Monorepos?
Am besten mit gemeinsamem Paket oder Workspace, sonst geht der Typ-Vorteil verloren.
3Queries vs. Mutations?
Queries fuer lesende, Mutations fuer schreibende Operationen, gleiche Input-Validierung.
4Andere Validierung als Zod moeglich?
Ja, jede Standard-Schema-kompatible Bibliothek wie Valibot oder ArkType funktioniert.
5Wie schuetze ich Procedures?
Ueber Auth-Middleware und einen protectedProcedure-Builder, der TRPCError bei fehlender Auth wirft.
6Wie werden Fehler uebertragen?
Ueber TRPCError mit festen Codes, automatisch auf HTTP-Status abgebildet.
7Eignet sich tRPC fuer oeffentliche APIs?
Nein, setzt TypeScript beidseitig voraus. REST/OpenAPI oder GraphQL sind besser geeignet.
8Wie kombiniert man tRPC mit React Query?
Ueber die offizielle Integration, die typisierte useQuery/useMutation-Hooks generiert.
9Was passiert bei Umbenennen einer Procedure?
Sofortiger Compilerfehler auf der Client-Seite an jeder betroffenen Stelle.
10tRPC und REST kombinierbar?
Ja, tRPC intern, REST/GraphQL fuer externe Konsumenten parallel im selben Projekt.