tRPC is the end-to-end type-safe API layer that eliminates the need for API contracts, code generation, and type duplication between your TypeScript backend and frontend. In 2026, tRPC v11 with React Query integration and Next.js App Router support has become the standard for full-stack TypeScript applications. This guide covers everything from setup to production patterns.
📋 Table of Contents
Why tRPC?
- Zero API contract — backend types automatically available on the frontend
- No code generation — types inferred at compile time, not generated
- Full TypeScript — autocomplete for API calls, arguments, and return types
- React Query built-in — caching, loading states, refetching work automatically
- Same DX as function call —
trpc.user.getById.useQuery(1)
Setup with 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);
Routers
// 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 Usage 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 in 2026 is the gold standard for full-stack TypeScript type safety. The DX improvement is dramatic — your IDE autocompletes API arguments and return types without any code generation step. Combined with Zod for input validation and Next.js App Router, tRPC creates the tightest possible TypeScript full-stack experience. Use it for any project where TypeScript is used on both client and server.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment