🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

tRPC Complete Guide 2026: Typsichere APIs mit Next.js und React Query

⏱️4 min read  ·  844 words

tRPC ist die durchgängig typsichere API-Schicht, die API-Verträge, Codegenerierung und Typduplizierung zwischen Ihrem TypeScript-Backend und Frontend überflüssig macht. Im Jahr 2026 ist tRPC v11 mit React Query-Integration und Next.js App Router-Unterstützung zum Standard für Full-Stack-TypeScript-Anwendungen geworden. Dieser Leitfaden deckt alles ab, von der Einrichtung bis hin zu Produktionsmustern.

Warum tRPC?

  • Kein API-Vertrag— Backend-Typen, die automatisch im Frontend verfügbar sind
  • Keine Codegenerierung– Typen, die zur Kompilierzeit abgeleitet und nicht generiert werden
  • Vollständiges TypeScript– Autovervollständigung für API-Aufrufe, Argumente und Rückgabetypen
  • React Query integriert– Zwischenspeichern, Laden von Zuständen, automatisches erneutes Abrufen von Arbeiten
  • Gleicher DX wie Funktionsaufruftrpc.user.getById.useQuery(1)

Einrichtung mit Next.js App Router

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

src/
  server/
    api/
      trpc.ts          — tRPC base config
      root.ts          — root router
      routers/
        user.ts
        post.ts
  trpc/
    react.tsx          — client setup
    server.ts          — server-side caller

Server-Setup

// server/api/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';

// Context type — available in all procedures
export interface Context {
  userId: string | null;
  db: PrismaClient;
}

const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

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

// Auth middleware
const isAuthenticated = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({ ctx: { ...ctx, userId: ctx.userId } });
});

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

// Rate limiting middleware
const withRateLimit = t.middleware(async ({ ctx, next }) => {
  const key = `ratelimit:${ctx.userId ?? 'anonymous'}`;
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, 60);
  if (count > 100) throw new TRPCError({ code: 'TOO_MANY_REQUESTS' });
  return next();
});

export const rateLimitedProcedure = protectedProcedure.use(withRateLimit);

Router

// server/api/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const userRouter = router({
  // Query — get data
  getById: publicProcedure
    .input(z.number().int().positive())
    .query(async ({ ctx, input }) => {
      const user = await ctx.db.user.findUnique({ where: { id: input } });
      if (!user) throw new TRPCError({ code: 'NOT_FOUND', message: `User ${input} not found` });
      return user;
    }),

  // Query with pagination
  list: publicProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().max(100).default(20),
      search: z.string().optional(),
    }))
    .query(async ({ ctx, input }) => {
      const { page, limit, search } = input;
      const [users, total] = await ctx.db.$transaction([
        ctx.db.user.findMany({
          where: search ? { name: { contains: search, mode: 'insensitive' } } : undefined,
          skip: (page - 1) * limit,
          take: limit,
        }),
        ctx.db.user.count(),
      ]);
      return { users, total, page, limit };
    }),

  // Mutation — create/update/delete
  create: protectedProcedure
    .input(z.object({
      name: z.string().min(2).max(50),
      email: z.string().email(),
    }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.user.create({ data: input });
    }),

  update: protectedProcedure
    .input(z.object({
      id: z.number(),
      name: z.string().optional(),
      email: z.string().email().optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input;
      return ctx.db.user.update({ where: { id }, data });
    }),

  // Subscription — real-time
  onUserCreated: protectedProcedure.subscription(({ ctx }) => {
    return observable<User>((emit) => {
      const unsub = eventEmitter.on('user.created', (user) => {
        emit.next(user);
      });
      return () => unsub();
    });
  }),
});

Root-Router

// server/api/root.ts
import { router } from './trpc';
import { userRouter } from './routers/user';
import { postRouter } from './routers/post';

export const appRouter = router({
  user: userRouter,
  post: postRouter,
});

export type AppRouter = typeof appRouter;

Next.js App Router Handler

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/api/root';
import { createContext } from '@/server/api/context';

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

export { handler as GET, handler as POST };

Client-Nutzung in React

// trpc/react.tsx
'use client';
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/api/root';

export const trpc = createTRPCReact<AppRouter>();

// components/UserList.tsx
'use client';
import { trpc } from '@/trpc/react';

export function UserList() {
  const { data, isLoading, error } = trpc.user.list.useQuery({
    page: 1,
    limit: 20,
    search: 'alice',
  });

  const createUser = trpc.user.create.useMutation({
    onSuccess: () => {
      utils.user.list.invalidate();  // refetch list
    },
  });

  const utils = trpc.useUtils();

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      {data?.users.map(user => <UserCard key={user.id} user={user} />)}
      <button onClick={() => createUser.mutate({ name: 'Bob', email: 'bob@example.com' })}>
        {createUser.isPending ? 'Creating...' : 'Add User'}
      </button>
    </div>
  );
}

// Server component usage (no hook)
import { api } from '@/trpc/server';

export default async function Page() {
  const users = await api.user.list.query({ page: 1, limit: 10 });
  return <UserList initialData={users} />;
}

tRPC im Jahr 2026 ist der Goldstandard für die Typsicherheit von Full-Stack-TypeScript. Die DX-Verbesserung ist dramatisch – Ihre IDE vervollständigt API-Argumente und Rückgabetypen automatisch, ohne dass ein Codegenerierungsschritt erforderlich ist. In Kombination mit Zod zur Eingabevalidierung und Next.js App Router schafft tRPC das engste mögliche TypeScript-Full-Stack-Erlebnis. Verwenden Sie es für jedes Projekt, bei dem TypeScript sowohl auf dem Client als auch auf dem Server verwendet wird.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা