Claude Code tRPC: Type-safe APIs ohne Codegen — End-to-End TypeScript mit KI
Wer einmal eine REST-API mit TypeScript gebaut hat, kennt das Problem: Frontend und Backend teilen denselben Typen — theoretisch. In der Praxis weichen sie auseinander. Ein Feld wird im Backend umbenannt, das Frontend weiß davon nichts, der Laufzeitfehler kommt irgendwann später. tRPC löst genau dieses Problem — ohne Codegen, ohne OpenAPI-Schema, ohne manuelle Typ-Synchronisation.
Mit Claude Code wird tRPC nochmal eine Stufe schärfer: Claude versteht den gesamten Typen-Graphen deines Projekts, erkennt wo Router und Client auseinanderlaufen, generiert Procedures mit passender Zod-Validation und hilft beim Aufbau von Middleware-Ketten — und das alles in einem Kontext, ohne dass du Dateien manuell hin und her kopierst.
Dieser Artikel zeigt, wie du tRPC in einer Next.js-Applikation mit Claude Code aufbaust — von der ersten Router-Definition bis zur React-Query-Integration im Frontend. Alle Code-Beispiele sind produktionsreif.
Claude Code Mastery — Full-Stack TypeScript, tRPC und mehr auf Deutsch
Der Kurs zeigt, wie du Claude Code für moderne TypeScript-Stacks produktiv einsetzt — tRPC, Next.js, Agents und Hooks. Einmalig bezahlt, kein Abo.
Zum Kurs — Jetzt starten → Einmalzahlung · Kein Abo · 14 Tage Rückgaberecht1. Was tRPC ist und warum es so gut zu Claude Code passt
tRPC steht für TypeScript Remote Procedure Call. Die Grundidee: Backend-Funktionen werden als Procedures definiert, der Client ruft diese Procedures auf — und TypeScript leitet die Typen automatisch vom Server zum Client. Kein separates Schema, kein Codegen-Step, keine manuelle Synchronisation.
Das klingt einfach, ist aber in der Praxis ein Paradigmenwechsel. Bei REST definiert man eine Route (GET /api/users/:id), beschreibt das Request- und Response-Format irgendwo (oder auch nicht), und hofft, dass Frontend und Backend übereinstimmen. Mit tRPC definiert man eine Procedure, und der TypeScript-Compiler gewährleistet Konsistenz — zur Compile-Zeit, nicht erst im Browser.
Warum passt das so gut zu Claude Code? Claude Code liest den gesamten Typen-Graphen deines Projekts in einem Schritt. Es sieht Router-Definition und Client-Aufruf gleichzeitig, erkennt Diskrepanzen sofort und kann Refactorings über beide Seiten hinweg konsistent durchführen — ohne dass du erklären musst, was wo definiert ist.
2. Projekt-Setup: tRPC in Next.js einrichten
Starte mit einem frischen Next.js-Projekt und installiere die nötigen Pakete. Claude Code kann das für dich übernehmen — gib einfach den Kontext:
claude "Richte tRPC v11 in diesem Next.js App-Router-Projekt ein.
Ich brauche: @trpc/server, @trpc/client, @trpc/react-query,
@tanstack/react-query und zod. Erstelle die Grundstruktur mit
server/trpc.ts, server/routers/_app.ts und app/api/trpc/[trpc]/route.ts."
Claude Code liest das bestehende Projekt, erkennt den App-Router (oder Pages-Router), wählt die passenden Adapter und erstellt die Grundstruktur. Kein manuelles Durchsuchen der tRPC-Dokumentation für die richtige Import-Struktur.
Das Ergebnis ist eine saubere Verzeichnisstruktur:
src/
server/
trpc.ts # tRPC-Instanz, Context, Middleware
routers/
_app.ts # Root-Router (mergeRouters)
users.ts # User-Procedures
posts.ts # Post-Procedures
app/
api/
trpc/
[trpc]/
route.ts # Next.js API-Route-Handler
_trpc/
client.ts # Client-Konfiguration
Provider.tsx # React Query Provider
Die tRPC-Instanz definieren
Die Kerndatei ist server/trpc.ts. Hier wird die tRPC-Instanz erstellt, der Context-Typ definiert und die Basis-Middleware eingerichtet:
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { type NextRequest } from 'next/server';
import { z } from 'zod';
import superjson from 'superjson';
// Context-Typ: was jede Procedure bekommt
export interface Context {
req: NextRequest;
session: Session | null;
db: PrismaClient;
}
// Context-Factory: wird pro Request aufgerufen
export async function createContext(req: NextRequest): Promise {
const session = await getServerSession(req);
return {
req,
session,
db: prisma,
};
}
// tRPC-Instanz mit Transformer (für Date, Map, Set etc.)
const t = initTRPC.context().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof z.ZodError
? error.cause.flatten()
: null,
},
};
},
});
// Exporte für Router und Procedures
export const router = t.router;
export const publicProcedure = t.procedure;
// Middleware: Auth-Check
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Du musst angemeldet sein.',
});
}
return next({
ctx: {
...ctx,
session: ctx.session, // session ist jetzt non-null
},
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
3. Router und Procedures definieren
Procedures sind das Herzstück von tRPC. Es gibt drei Typen: query (Daten lesen), mutation (Daten schreiben) und subscription (Echtzeit-Updates). Claude Code hilft dabei, die richtigen Zod-Schemas zu definieren und die Business-Logik sauber zu trennen:
claude "Erstelle einen User-Router mit diesen Procedures:
- getById: query, nimmt eine userId, gibt User-Objekt zurück
- getAll: protected query, gibt alle User mit Pagination zurück
- create: protected mutation, validiert Name und Email via Zod
- update: protected mutation, partial update, nur eigene User
- delete: protected mutation, nur eigene User oder Admin
Verwende Prisma als ORM und wirf TRPCError mit passendem Code
wenn etwas nicht gefunden wird oder Rechte fehlen."
Claude Code generiert einen vollständigen Router:
// server/routers/users.ts
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import {
router,
publicProcedure,
protectedProcedure,
} from '../trpc';
const userCreateSchema = z.object({
name: z.string().min(2, 'Name muss mindestens 2 Zeichen haben'),
email: z.string().email('Ungültige E-Mail-Adresse'),
role: z.enum(['USER', 'ADMIN']).default('USER'),
});
const paginationSchema = z.object({
page: z.number().int().min(1).default(1),
limit: z.number().int().min(1).max(100).default(20),
});
export const usersRouter = router({
// Öffentliche Query: einzelner User
getById: publicProcedure
.input(z.object({ id: z.string().cuid() }))
.query(async ({ ctx, input }) => {
const user = await ctx.db.user.findUnique({
where: { id: input.id },
select: { id: true, name: true, createdAt: true },
});
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `User ${input.id} nicht gefunden`,
});
}
return user;
}),
// Geschützte Query: alle User mit Pagination
getAll: protectedProcedure
.input(paginationSchema)
.query(async ({ ctx, input }) => {
const { page, limit } = input;
const skip = (page - 1) * limit;
const [users, total] = await ctx.db.$transaction([
ctx.db.user.findMany({
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
ctx.db.user.count(),
]);
return {
users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}),
// Geschützte Mutation: User anlegen
create: protectedProcedure
.input(userCreateSchema)
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db.user.findUnique({
where: { email: input.email },
});
if (existing) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Diese E-Mail-Adresse ist bereits vergeben',
});
}
return ctx.db.user.create({ data: input });
}),
// Geschützte Mutation: User aktualisieren
update: protectedProcedure
.input(z.object({
id: z.string().cuid(),
data: userCreateSchema.partial(),
}))
.mutation(async ({ ctx, input }) => {
const user = await ctx.db.user.findUnique({
where: { id: input.id },
});
if (!user) {
throw new TRPCError({ code: 'NOT_FOUND' });
}
const isOwner = user.id === ctx.session.user.id;
const isAdmin = ctx.session.user.role === 'ADMIN';
if (!isOwner && !isAdmin) {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return ctx.db.user.update({
where: { id: input.id },
data: input.data,
});
}),
});
4. Middleware: Logging, Rate Limiting und Auth
Middleware in tRPC ist mächtig und composable. Claude Code hilft dabei, Middleware-Ketten aufzubauen, die sich konsistent über alle Procedures hinweg verhalten:
// server/trpc.ts (erweitert)
import { TRPCError } from '@trpc/server';
// Logging-Middleware
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
console.log(
`[tRPC] ${type} ${path} - ${result.ok ? 'OK' : 'ERROR'} (${duration}ms)`
);
return result;
});
// Rate-Limiting-Middleware (einfache Implementierung)
const rateLimitMap = new Map<string, number[]>();
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
const identifier = ctx.session?.user?.id ?? ctx.req.ip ?? 'anonymous';
const now = Date.now();
const windowMs = 60_000; // 1 Minute
const maxRequests = 60;
const timestamps = rateLimitMap.get(identifier) ?? [];
const recent = timestamps.filter((t) => now - t < windowMs);
if (recent.length >= maxRequests) {
throw new TRPCError({
code: 'TOO_MANY_REQUESTS',
message: 'Zu viele Anfragen. Bitte warte einen Moment.',
});
}
rateLimitMap.set(identifier, [...recent, now]);
return next();
});
// Kombinierte Procedure mit beiden Middlewares
export const publicProcedure = t.procedure
.use(loggerMiddleware)
.use(rateLimitMiddleware);
export const protectedProcedure = t.procedure
.use(loggerMiddleware)
.use(rateLimitMiddleware)
.use(isAuthed);
Praxishinweis: Für produktives Rate-Limiting solltest du Redis verwenden statt einer In-Memory-Map, damit das Limit auch bei mehreren Server-Instanzen gilt. Claude Code kann dir dabei helfen, eine Redis-basierte Implementierung mit ioredis aufzubauen.
5. React Query Integration im Frontend
tRPC integriert sich nahtlos mit TanStack Query (ehemals React Query). Das Ergebnis: automatisches Caching, Loading-States, Optimistic Updates — alles type-safe. Claude Code generiert nicht nur die Server-Seite, sondern auch die passenden Client-Hooks:
// app/_trpc/client.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';
export const trpc = createTRPCReact<AppRouter>();
// app/_trpc/Provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink, loggerLink } from '@trpc/client';
import superjson from 'superjson';
import { useState } from 'react';
import { trpc } from './client';
export function TRPCProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 Minute Cache
retry: (failureCount, error: any) => {
// Keine Wiederholung bei Auth-Fehlern
if (error?.data?.code === 'UNAUTHORIZED') return false;
return failureCount < 3;
},
},
},
})
);
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error),
}),
httpBatchLink({
url: '/api/trpc',
transformer: superjson,
headers() {
return { 'x-trpc-source': 'react' };
},
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}
Queries und Mutations in Komponenten nutzen
Im Component selbst fühlt sich tRPC an wie ein typisierter API-Client, der direkt im Editor autocompletet:
'use client';
import { trpc } from '@/app/_trpc/client';
import { useState } from 'react';
export function UserList() {
const [page, setPage] = useState(1);
// Type-safe Query: Input und Output sind vollständig typisiert
const { data, isLoading, error } = trpc.users.getAll.useQuery({
page,
limit: 20,
});
// Type-safe Mutation mit Optimistic Update
const utils = trpc.useUtils();
const createUser = trpc.users.create.useMutation({
onSuccess: () => {
// Cache invalidieren nach erfolgreicher Mutation
utils.users.getAll.invalidate();
},
onError: (error) => {
// Strukturierter Zod-Fehler aus errorFormatter
if (error.data?.zodError) {
console.error('Validierungsfehler:', error.data.zodError.fieldErrors);
}
},
});
if (isLoading) return <div>Lädt...</div>;
if (error) return <div>Fehler: {error.message}</div>;
return (
<div>
<ul>
{data?.users.map((user) => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
<div>
Seite {data?.pagination.page} von {data?.pagination.totalPages}
<button
onClick={() => setPage((p) => p - 1)}
disabled={page === 1}
>
Zurück
</button>
<button
onClick={() => setPage((p) => p + 1)}
disabled={page === data?.pagination.totalPages}
>
Weiter
</button>
</div>
</div>
);
}
6. Zod Validation: Schemas durchgängig nutzen
Zod ist das Validierungs-Framework, das tRPC standardmäßig nutzt. Der entscheidende Vorteil: Du definierst das Schema einmal und nutzt es auf mehreren Ebenen. Claude Code versteht das und hilft dir dabei, Schemas konsistent durch deine Applikation zu ziehen:
// shared/schemas/user.ts
// Dieses Schema wird auf Server UND Client verwendet
import { z } from 'zod';
export const userSchema = z.object({
id: z.string().cuid(),
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(['USER', 'ADMIN', 'MODERATOR']),
bio: z.string().max(500).nullable(),
createdAt: z.date(),
updatedAt: z.date(),
});
// Abgeleitete Typen — kein separates Interface nötig
export type User = z.infer<typeof userSchema>;
// Create-Schema: bestimmte Felder weglassen
export const createUserSchema = userSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
}).extend({
password: z.string().min(8).max(100),
});
// Update-Schema: alles optional ausser id
export const updateUserSchema = userSchema
.partial()
.required({ id: true })
.omit({ createdAt: true, updatedAt: true });
// Response-Schema: Passwort niemals zurückgeben
export const userResponseSchema = userSchema.omit({ bio: true });
export type UserResponse = z.infer<typeof userResponseSchema>;
Claude Code kann dieses Muster in deiner gesamten Applikation konsistent anwenden. Wenn du ein Schema änderst, erkennt Claude Code alle Stellen, die davon betroffen sind:
claude "Das userSchema hat jetzt ein neues Pflichtfeld 'department: string'.
Zeig mir alle Stellen im Projekt, die davon betroffen sind, und aktualisiere
sie konsistent — Server-Router, Client-Formulare und Tests."
7. Server-Side Rendering und Server Components
Mit dem Next.js App Router können Server Components direkt auf den tRPC-Router zugreifen — ohne HTTP-Anfrage, direkt als Funktionsaufruf. Das ist schneller und vermeidet doppelte Daten-Ladevorgänge:
// app/users/page.tsx (Server Component)
import { createContext } from '@/server/trpc';
import { appRouter } from '@/server/routers/_app';
import { createCallerFactory } from '@trpc/server';
const createCaller = createCallerFactory(appRouter);
export default async function UsersPage() {
// Direkter Aufruf ohne HTTP — für Server Components
const ctx = await createContext(/* req aus headers() */);
const caller = createCaller(ctx);
// Vollständig typisiert, kein Fetch nötig
const { users, pagination } = await caller.users.getAll({
page: 1,
limit: 20,
});
return (
<main>
<h1>Benutzer ({pagination.total})</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</main>
);
}
Hybrides Pattern: Nutze Server Components für den initialen Datenladevorgang (schnell, kein Flicker), und React Query für interaktive Updates danach. tRPC unterstützt Prefetching, sodass der Server die Daten lädt und der Client sie aus dem Cache nimmt — ohne doppelten Request.
8. Claude Code im tRPC-Workflow: konkrete Prompts
Der größte Vorteil von Claude Code bei tRPC ist die bidirektionale Konsistenz: Claude Code sieht Router und Client gleichzeitig und kann Änderungen atomar durchführen. Hier sind konkrete Prompts für typische Szenarien:
Neuen Router hinzufügen
claude "Füge einen neuen 'posts'-Router hinzu mit:
- getAll: paginierte Query aller Posts (public)
- getBySlug: Query per Slug (public)
- create: Mutation (protected), Felder: title, content, slug, tags[]
- publish: Mutation (protected, nur Autor oder Admin)
- delete: Mutation (protected, nur Autor oder Admin)
Registriere den Router in _app.ts und erstelle die nötigen
Zod-Schemas in shared/schemas/post.ts."
Bestehende Procedure erweitern
claude "Die getAll-Procedure im users-Router soll jetzt filtern können.
Füge optionale Filter hinzu: role, search (Name oder Email), createdAfter.
Aktualisiere das Input-Schema, die Prisma-Query und den Frontend-Hook
in components/UserList.tsx gleichzeitig."
Fehler diagnostizieren
claude "Ich bekomme diesen tRPC-Fehler im Client:
TRPCClientError: No \"query\"-procedure on path \"users.getProfile\"
Zeig mir, was falsch ist und wie ich es behebe."
Claude Code liest daraufhin den Router, findet die fehlende oder falsch benannte Procedure und zeigt dir den genauen Fix — inklusive ob das Problem auf der Server-Seite oder beim Client-Aufruf liegt.
9. Testing: tRPC-Procedures testen
tRPC-Procedures lassen sich mit dem createCaller-Pattern direkt testen, ohne HTTP-Server. Claude Code schreibt diese Tests in einem Schritt:
// tests/routers/users.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createCallerFactory } from '@trpc/server';
import { appRouter } from '@/server/routers/_app';
import { createMockContext } from '@/tests/helpers/context';
const createCaller = createCallerFactory(appRouter);
describe('users.create', () => {
let mockCtx: ReturnType<typeof createMockContext>;
let caller: ReturnType<typeof createCaller>;
beforeEach(() => {
mockCtx = createMockContext({ role: 'ADMIN' });
caller = createCaller(mockCtx);
});
it('erstellt einen neuen User', async () => {
const input = {
name: 'Max Mustermann',
email: 'max@example.com',
};
const result = await caller.users.create(input);
expect(result.name).toBe(input.name);
expect(result.email).toBe(input.email);
expect(result.id).toBeDefined();
});
it('wirft CONFLICT bei doppelter E-Mail', async () => {
// Zuerst anlegen
await caller.users.create({
name: 'Erster User',
email: 'doppelt@example.com',
});
// Zweites Mal muss CONFLICT werfen
await expect(
caller.users.create({
name: 'Zweiter User',
email: 'doppelt@example.com',
})
).rejects.toMatchObject({ code: 'CONFLICT' });
});
it('wirft UNAUTHORIZED ohne Session', async () => {
const unauthCtx = createMockContext({ session: null });
const unauthCaller = createCaller(unauthCtx);
await expect(
unauthCaller.users.create({
name: 'Kein Zugriff',
email: 'test@example.com',
})
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
});
});
Mit claude "Schreib Tests für alle Procedures im users-Router" generiert Claude Code eine vollständige Test-Suite — mit Happy-Path, Fehlerszenarien und Edge-Cases. Das spart nicht nur Schreibarbeit, sondern stellt auch sicher, dass die Tests tatsächlich die Fehlercodes prüfen, die tRPC wirft.
10. Performance: Request Batching und Caching
tRPC hat Request Batching eingebaut: Wenn mehrere Queries in kurzer Zeit gefeuert werden, bündelt der Client sie in einem einzigen HTTP-Request. Das reduziert die Netzwerklast erheblich, besonders bei vielen kleinen Queries.
Claude Code kann dir helfen, das Caching-Verhalten pro Procedure zu konfigurieren und Response-Caching für öffentliche Daten einzurichten:
// server/routers/posts.ts
export const postsRouter = router({
// Öffentliche Query mit HTTP-Caching
getAll: publicProcedure
.input(paginationSchema)
.query(async ({ ctx, input }) => {
// Next.js fetch-Cache für ISR
const posts = await fetch('/internal/posts', {
next: { revalidate: 60 }, // 60 Sekunden Cache
}).then((r) => r.json());
return posts;
}),
// Oder: direkt mit Prisma und Cache-Tags
getBySlug: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.db.post.findUnique({
where: { slug: input.slug },
// Prisma Accelerate Cache (optional)
cacheStrategy: { ttl: 300 },
});
}),
});
Zwei verwandte Artikel, die auf diesem Thema aufbauen:
- Claude Code Debugging — Bugs in TypeScript-Projekten systematisch finden und beheben
- Claude Code für Unternehmen — Deployment, Zugriffskontrolle und Team-Workflows
Claude Code Mastery — tRPC, Next.js und Full-Stack TypeScript auf Deutsch
Im Kurs lernst du, wie du Claude Code für moderne TypeScript-Stacks einsetzt: tRPC, Next.js App Router, Agents, MCP-Server und Hooks. Einmalig bezahlt, kein Abo.
Jetzt starten → Basis ab €29 · Pro ab €49 · 14 Tage RückgaberechtKurs · Claude Code Mastery
Full-Stack TypeScript mit KI — tRPC, Next.js, Agents
tRPC. Zod. React Query. Claude Code Agents. Alles auf Deutsch, einmalig bezahlt — kein Abo, keine Plattformabhängigkeit.
Basis ab €29 · Pro ab €49 · 14 Tage Rückgaberecht