Use Cases (Aktualisiert: 2.6.2026)

Typsichere tRPC-API mit Claude Code bauen

Baue eine typsichere tRPC-API mit Claude Code: Next.js, Zod, Rollen, Cache-Invalidierung und Review-Prompts.

Typsichere tRPC-API mit Claude Code bauen

Warum tRPC und Claude Code zusammenpassen

tRPC macht den TypeScript-Router auf der Serverseite zur gemeinsamen API-Vertragsquelle. Der Client braucht keine separate OpenAPI-Datei und kein manuell gepflegtes SDK, sondern importiert den Typ AppRouter und erhält passende Eingaben, Rückgaben und Fehlerformen. Das ist besonders wertvoll, wenn UI und API in einem Repository schnell gemeinsam wachsen.

Claude Code ist nicht nur für Boilerplate nützlich. Der eigentliche Nutzen entsteht, wenn es die vorhandene Projektstruktur, Authentifizierung, Datenzugriffe, React-Query-Konventionen und Namensregeln lesen kann. Dann kann es Router, Procedures, Zod-Validierung, Autorisierung und Cache-Invalidierung so erzeugen, dass sie in das bestehende System passen. Trotzdem gilt: Typsicherheit ersetzt keine fachliche Prüfung. Eine Mutation kann korrekt typisiert sein und trotzdem Daten eines fremden Teams ändern, wenn die Berechtigungsprüfung fehlt.

Das Beispiel nutzt Next.js App Router und eine kleine Projektverwaltungs-API. Die Daten liegen in einem Map, damit der Code kopierbar bleibt. In einer echten Anwendung ersetzt du diesen Teil durch Prisma, Drizzle, Supabase oder eine vorhandene Datenquelle. Die Demo-Session muss vor Produktion ebenfalls durch echte Authentifizierung ersetzt werden.

Geeignete Use Cases

tRPC passt am besten, wenn Server und Client im gleichen TypeScript-System leben. Für öffentliche APIs mit externen Partnern kann OpenAPI besser sein. Für Admin-Oberflächen, interne Tools, Formulare und dünne BFF-Schichten ist tRPC oft sehr produktiv.

Use CaseVorteil von tRPCAufgabe für Claude CodeRisiko
Admin-CRUDEingaben und Rückgaben für Listen, Erstellen, Ändern, Löschen bleiben verbundenRouter, Zod-Schema, Mutations, InvalidierungButtons verstecken statt serverseitig prüfen
Interne ToolsProzessänderungen können schnell mit UI und API ausgeliefert werdenProcedures aus bestehenden Modellen ableitenZu großer context
FormulareRuntime-Validierung für E-Mail, Länge und enumFehlermeldungen und Schutz vor DoppelversandNur TypeScript vertrauen
Dünner BFFExakt die Datenform für die Seite liefernExterne API-Antworten mappenCache-Regeln unklar lassen
flowchart LR
  UI["React component"]
  Client["tRPC React client"]
  Router["AppRouter"]
  Procedure["protected/admin procedure"]
  Zod["Zod validation"]
  Store["DB or store"]
  Review["Claude Code review"]

  UI --> Client --> Router --> Procedure --> Zod --> Store
  Router --> Review
  Review --> Procedure

Installation und Struktur

Installiere zuerst die Pakete. Falls React Query oder Zod bereits vorhanden sind, gleiche die Versionen ab, bevor Claude Code Code erzeugt.

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod superjson

Die folgende Struktur trennt Servercode, HTTP-Adapter und React-Provider klar.

src/
  app/api/trpc/[trpc]/route.ts
  app/projects/project-list.tsx
  server/trpc.ts
  server/routers/_app.ts
  server/routers/project.ts
  trpc/client.tsx

Gib Claude Code diese Grenze ausdrücklich mit: Serverdateien importieren keine Client Components, Client Components greifen nicht direkt auf den Store zu, und gemeinsame Typen kommen aus dem tRPC-Router statt aus parallelen DTO-Dateien.

Context und Procedures

Der context enthält request-bezogene Informationen für jede Procedure. Halte ihn klein: Session, Team, Rolle und Datenzugriff reichen meistens. Der folgende Code nutzt Header nur als lokale Demo.

// src/server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";

type Role = "admin" | "member";
type Session = { userId: string; teamId: string; role: Role };
export type Context = { session: Session | null };

export async function createContext({
  headers,
}: {
  headers: Headers;
}): Promise<Context> {
  const roleHeader = headers.get("x-user-role");
  const role: Role =
    roleHeader === "admin"
      ? "admin"
      : roleHeader === "member"
        ? "member"
        : process.env.NODE_ENV === "production"
          ? "member"
          : "admin";

  return {
    session: {
      userId: headers.get("x-user-id") ?? "demo-user",
      teamId: headers.get("x-team-id") ?? "demo-team",
      role,
    },
  };
}

const t = initTRPC.context<Context>().create({ transformer: superjson });

export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;

const requireUser = t.middleware(({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({ code: "UNAUTHORIZED", message: "Login is required." });
  }
  return next({ ctx: { session: ctx.session } });
});

export const protectedProcedure = t.procedure.use(requireUser);

export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
  if (ctx.session.role !== "admin") {
    throw new TRPCError({ code: "FORBIDDEN", message: "Admin role is required." });
  }
  return next();
});

Die Trennung zwischen protectedProcedure und adminProcedure macht Reviews einfacher. Eine gefährliche Mutation mit dem falschen Procedure-Typ fällt sofort auf.

Router mit Zod-Runtime-Validierung

TypeScript prüft den Build, aber nicht fremdes JSON zur Laufzeit. Zod validiert Eingaben genau an der API-Grenze, bevor fachliche Operationen laufen.

// src/server/routers/project.ts
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";

type ProjectStatus = "todo" | "doing" | "done";
type Project = {
  id: string;
  teamId: string;
  title: string;
  ownerEmail: string;
  status: ProjectStatus;
  createdAt: string;
};

const projects = new Map<string, Project>();
const projectStatus = z.enum(["todo", "doing", "done"]);

const listProjectsInput = z
  .object({
    status: projectStatus.optional(),
    query: z.string().trim().max(60).optional(),
    limit: z.number().int().min(1).max(50).default(20),
  })
  .default({ limit: 20 });

const createProjectInput = z.object({
  title: z.string().trim().min(2).max(80),
  ownerEmail: z.string().email(),
});

export const projectRouter = createTRPCRouter({
  list: protectedProcedure.input(listProjectsInput).query(({ ctx, input }) => {
    return [...projects.values()]
      .filter((project) => project.teamId === ctx.session.teamId)
      .filter((project) => !input.status || project.status === input.status)
      .filter((project) => !input.query || project.title.includes(input.query))
      .slice(0, input.limit);
  }),

  create: adminProcedure.input(createProjectInput).mutation(({ ctx, input }) => {
    const project: Project = {
      id: crypto.randomUUID(),
      teamId: ctx.session.teamId,
      title: input.title,
      ownerEmail: input.ownerEmail,
      status: "todo",
      createdAt: new Date().toISOString(),
    };
    projects.set(project.id, project);
    return project;
  }),

  updateStatus: protectedProcedure
    .input(z.object({ id: z.string().uuid(), status: projectStatus }))
    .mutation(({ ctx, input }) => {
      const project = projects.get(input.id);
      if (!project || project.teamId !== ctx.session.teamId) {
        throw new TRPCError({ code: "NOT_FOUND" });
      }
      const nextProject = { ...project, status: input.status };
      projects.set(input.id, nextProject);
      return nextProject;
    }),
});

Die Prüfung von teamId ist hier entscheidend. Ein gültiger UUID-String beweist nicht, dass der aktuelle Benutzer diesen Datensatz bearbeiten darf.

App Router und React Client

Fasse den Router zusammen und stelle ihn als Route Handler bereit.

// src/server/routers/_app.ts
import { createTRPCRouter } from "../trpc";
import { projectRouter } from "./project";

export const appRouter = createTRPCRouter({
  project: projectRouter,
});

export type AppRouter = typeof appRouter;
// src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/trpc";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => createContext({ headers: req.headers }),
  });

export { handler as GET, handler as POST };
// src/trpc/client.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { useState, type ReactNode } from "react";
import superjson from "superjson";
import type { AppRouter } from "@/server/routers/_app";

export const trpc = createTRPCReact<AppRouter>();

export function TRPCProvider({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      transformer: superjson,
      links: [httpBatchLink({ url: "/api/trpc" })],
    }),
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}
// src/app/projects/project-list.tsx
"use client";

import { useState } from "react";
import { trpc } from "@/trpc/client";

export function ProjectList() {
  const utils = trpc.useUtils();
  const [title, setTitle] = useState("");
  const projects = trpc.project.list.useQuery({ limit: 20 });
  const createProject = trpc.project.create.useMutation({
    onSuccess: async () => {
      setTitle("");
      await utils.project.list.invalidate();
    },
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        createProject.mutate({ title, ownerEmail: "owner@example.com" });
      }}
    >
      <input value={title} onChange={(event) => setTitle(event.target.value)} />
      <button type="submit">Add</button>
      <pre>{JSON.stringify(projects.data, null, 2)}</pre>
    </form>
  );
}

Nach einer erfolgreichen Mutation wird project.list invalidiert. Ohne diese Regel bleibt die UI oft sichtbar veraltet, obwohl die Serveroperation erfolgreich war.

Review-Prompt für Claude Code

Reviewe diese Next.js App Router + tRPC Implementierung.
Prüfe:
1. publicProcedure, protectedProcedure und adminProcedure korrekt verwendet.
2. Zod-Runtime-Validierung für alle externen Inputs.
3. teamId und userId vor Lesen und Schreiben geprüft.
4. Betroffene Queries nach Mutations invalidiert.
5. Router nach Domänen getrennt und nicht zu groß.
6. Context enthält nur request-bezogene Abhängigkeiten.
7. Keine geheimen Felder werden an den Client zurückgegeben.

Gib Schweregrad, Datei, Problem, Fix und empfohlene Tests als Tabelle aus.

Dieser Prompt passt in CLAUDE.md oder in ein Pull-Request-Template. Bei Änderungen an Berechtigungen sollte Claude Code zuerst nur Diff und Risikoanalyse liefern.

Typische Fehler

Erstens: ein aufgeblähter context. Je mehr dort landet, desto schwerer werden Tests. Zweitens: Autorisierung nur in der UI. Ein versteckter Button schützt keinen Endpoint. Drittens: TypeScript ohne Zod. Echte Requests müssen zur Laufzeit geprüft werden. Viertens: ein monolithischer Router. Nach wenigen Wochen sinkt die Review-Qualität. Fünftens: unklare Cache-Invalidierung. Eine Mutation kann Listen, Details, Zähler und Dashboards betreffen; diese Abhängigkeiten sollten vor dem Code notiert werden.

Offizielle Quellen und nächste Schritte

Nutze die tRPC-Dokumentation zu Routers, Procedures und React Query integration. Für Runtime-Validierung ist die Zod-Dokumentation relevant, für den HTTP-Einstieg die Next.js Route Handlers.

Als nächste interne Links passen der Claude Code Zod Validation Guide und die Claude Code TypeScript Tipps. Wenn dein Team ein bestehendes Admin-Tool oder internes System prüfen möchte, unterstützt Claude Code Training und Beratung bei Router-Schnitt, Rollenmodell, Review-Prompts und Tests.

Ergebnis aus dem Test

Im Prototyp einer Admin-Oberfläche war die wichtigste Entscheidung, protectedProcedure und adminProcedure vor den CRUD-Routern anzulegen. Claude Code konnte dadurch schnell markieren, welche Mutation strengere Rechte braucht. Zod-Schemas direkt neben den Procedures machten Formularänderungen leichter nachvollziehbar. Der Nachteil zeigte sich bei einem zu bequemen context: Tests wurden schwerfällig. Die bessere Variante hält den context klein und verschiebt Fachlogik in separate Funktionen.

#Claude Code #tRPC #TypeScript #API #type safety
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.