tRPC ermöglicht dir, typsichere APIs in TypeScript zu bauen, ganz ohne Schema-Definitionen, Code-Generierung oder REST-Boilerplate. Dein Server definiert Prozeduren, und der Client kennt automatisch alle Typen. Einfach, schnell und sicher.
Was ist tRPC? 🤔
Stell dir vor, du könntest eine API bauen, bei der dein Client automatisch weiß, welche Endpunkte es gibt, welche Parameter sie erwarten und was sie zurückgeben, ohne eine einzige Schema-Datei zu schreiben. Genau das macht tRPC.
tRPC steht für TypeScript Remote Procedure Call. Es nutzt die volle Power von TypeScripts Typsystem, um End-to-End-Typsicherheit zwischen Server und Client zu garantieren. Keine OpenAPI-Specs, keine GraphQL-Schemas, keine Code-Generierung, nur pures TypeScript.
End-to-End Type Safety erklärt 🔒
Das Kernprinzip von tRPC ist einfach: Du definierst deine API-Logik auf dem Server, und der Client leitet alle Typen automatisch daraus ab. Kein manuelles Synchronisieren von Typen, kein Vergessen von Feldern.
Das funktioniert, weil tRPC den typeof-Operator und TypeScripts Type Inference nutzt. Der Server exportiert einen Typ (den sogenannten AppRouter), und der Client importiert diesen Typ, nicht den Code, nur den Typ. Dadurch weiß der Client zur Compile-Zeit exakt, was möglich ist.
Router und Prozeduren 🛤️
In tRPC dreht sich alles um Router und Prozeduren. Ein Router ist im Grunde eine Sammlung von API-Endpunkten, und eine Prozedur ist ein einzelner Endpunkt.
Es gibt drei Arten von Prozeduren:
- Query, Zum Lesen von Daten (wie GET in REST)
- Mutation, Zum Schreiben/Ändern von Daten (wie POST/PUT/DELETE)
- Subscription, Für Echtzeit-Updates via WebSockets
Hier ein einfaches Beispiel für einen Router mit verschiedenen Prozeduren:
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const appRouter = t.router({
// Einfache Query ohne Input
hello: t.procedure.query(() => {
return { message: 'Hallo Welt!' };
}),
// Query mit Input
getUserById: t.procedure
.input((val: unknown) => {
if (typeof val === 'string') return val;
throw new Error('Ungültige ID');
})
.query(({ input }) => {
// input ist hier automatisch als string getypt
return { id: input, name: 'Max Mustermann' };
}),
// Mutation
createUser: t.procedure
.input((val: unknown) => {
// Einfache Validierung
const obj = val as { name: string; email: string };
return obj;
})
.mutation(({ input }) => {
// User erstellen...
return { id: '1', ...input };
}),
});
// Diesen Typ exportierst du für den Client
export type AppRouter = typeof appRouter;Input-Validierung mit Zod ✅
Die manuelle Input-Validierung im obigen Beispiel ist natürlich nicht ideal. Hier kommt Zod ins Spiel, eine Runtime-Validierungsbibliothek, die perfekt mit tRPC harmoniert.
Mit Zod sieht die Input-Validierung so aus:
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
createUser: t.procedure
.input(
z.object({
name: z.string().min(2, 'Name muss mindestens 2 Zeichen haben'),
email: z.string().email('Ungültige E-Mail-Adresse'),
age: z.number().min(18).optional(),
})
)
.mutation(({ input }) => {
// input ist jetzt vollständig getypt:
// { name: string; email: string; age?: number }
return { id: crypto.randomUUID(), ...input };
}),
getUsers: t.procedure
.input(
z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
})
)
.query(({ input }) => {
// Pagination mit typsicherem Input
return {
users: [],
nextCursor: null,
};
}),
});Das Schöne daran: Zod validiert die Daten zur Laufzeit und leitet gleichzeitig die TypeScript-Typen ab. Du bekommst also Compile-Zeit- und Runtime-Sicherheit in einem.
Client-Nutzung 📱
Auf der Client-Seite ist tRPC extrem elegant. Du erstellst einen typisierten Client und rufst deine Prozeduren einfach wie normale Funktionen auf:
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
// Client erstellen
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
}),
],
});
// Queries aufrufen – voll typisiert!
const greeting = await trpc.hello.query();
console.log(greeting.message); // "Hallo Welt!"
// Mit Input
const user = await trpc.getUserById.query('123');
console.log(user.name); // Typ ist automatisch bekannt
// Mutation aufrufen
const newUser = await trpc.createUser.mutate({
name: 'Anna',
email: '[email protected]',
age: 25,
});
// newUser ist voll getypt: { id: string; name: string; email: string; age?: number }Das Beste: Wenn du auf dem Server einen Parameter umbenennst oder einen Endpunkt änderst, zeigt dir dein Editor sofort alle Stellen im Client, die angepasst werden müssen. Kein Raten, kein Debuggen von Runtime-Fehlern.
Middleware 🧱
Middleware in tRPC ist unglaublich mächtig. Du kannst damit Authentifizierung, Logging, Rate-Limiting und mehr implementieren, und das alles typsicher:
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
interface Context {
user?: { id: string; role: string };
}
const t = initTRPC.context<Context>().create();
// Auth-Middleware
const isAuthenticated = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Du musst eingeloggt sein',
});
}
return next({
ctx: {
// ctx.user ist jetzt garantiert vorhanden
user: ctx.user,
},
});
});
// Logging-Middleware
const withLogging = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
console.log(`${type} ${path} - ${duration}ms`);
return result;
});
// Geschützte Prozedur erstellen
const protectedProcedure = t.procedure
.use(withLogging)
.use(isAuthenticated);
const appRouter = t.router({
// Öffentlich
getPublicData: t.procedure.query(() => {
return { data: 'Öffentlich zugänglich' };
}),
// Geschützt – nur mit Auth
getSecretData: protectedProcedure.query(({ ctx }) => {
// ctx.user ist hier garantiert definiert!
return { secret: `Geheim für ${ctx.user.id}` };
}),
updateProfile: protectedProcedure
.input(z.object({ name: z.string() }))
.mutation(({ ctx, input }) => {
return { userId: ctx.user.id, newName: input.name };
}),
});Durch die Middleware-Kette werden Typen schrittweise verfeinert. Nach der Auth-Middleware weiß TypeScript, dass ctx.user existiert, ohne manuelle Type-Guards.
tRPC vs. REST vs. GraphQL ⚖️
| Kriterium | REST | GraphQL | tRPC |
|---|---|---|---|
| Typsicherheit | Manuell (OpenAPI) | Schema-basiert | Automatisch (TypeScript) |
| Code-Generierung | Oft nötig | Oft nötig | Nicht nötig |
| Lernkurve | Niedrig | Mittel-Hoch | Niedrig |
| Sprachen | Alle | Alle | Nur TypeScript |
| Overfetching | Häufig | Gelöst | Kein Problem* |
| Schema-Datei | Optional (OpenAPI) | Erforderlich | Nicht nötig |
| Ökosystem | Riesig | Groß | Wachsend |
| Ideal für | Öffentliche APIs | Komplexe Datenabfragen | Full-Stack TypeScript |
* Da du die Rückgabewerte selbst definierst, gibt es kein Overfetching-Problem im klassischen Sinne.
Wann solltest du tRPC nutzen? 🎯
tRPC ist perfekt für dich, wenn:
- Du ein Full-Stack TypeScript-Projekt hast (z.B. Next.js, Nuxt, SvelteKit)
- Server und Client im selben Monorepo leben
- Du schnell iterieren willst, ohne Schema-Dateien zu pflegen
- Typsicherheit für dich nicht verhandelbar ist
tRPC ist weniger geeignet, wenn:
- Deine API von Nicht-TypeScript-Clients konsumiert wird (Mobile Apps in Swift/Kotlin, andere Backend-Services)
- Du eine öffentliche API anbietest, die von Dritten genutzt wird
- Du verschiedene Sprachen im Backend und Frontend einsetzt
Für öffentliche APIs ist REST mit OpenAPI oder GraphQL oft die bessere Wahl. Aber für interne Full-Stack-Projekte mit TypeScript? Da ist tRPC aktuell kaum zu schlagen.
Fazit 🏁
tRPC ist ein Game-Changer für TypeScript-Entwickler. Es eliminiert die Lücke zwischen Server und Client, indem es TypeScripts Typsystem maximal ausnutzt. Kein Boilerplate, keine Code-Generierung, keine Schema-Synchronisation, nur typsichere Prozeduraufrufe.
Wenn du bereits ein TypeScript-Full-Stack-Setup hast, solltest du tRPC definitiv ausprobieren. Die Developer Experience ist hervorragend, und die Typsicherheit gibt dir ein Vertrauen in deinen Code, das du mit REST oder GraphQL nur mit erheblichem Mehraufwand erreichst.
Probier es aus, du wirst dich fragen, warum du jemals manuell API-Typen synchronisiert hast! 🚀