tRPC (Type-Safe API Layer)
Overview
tRPC enables end-to-end typesafe APIs by sharing TypeScript types between client and server. No code generation, no schema files—just TypeScript.
Version: v11.7+ (2024-2025)
Requirements: TypeScript ≥5.7.2 with strict mode
Key Benefit: Change a procedure's input/output → TypeScript errors appear immediately on client.
When to Use This Skill
✅ Use tRPC when:
-
Building full-stack TypeScript apps (Next.js, React + Express)
-
You control both client and server code
-
Need type-safe API without GraphQL complexity
-
Want automatic request batching and caching
-
Building internal APIs, dashboards, admin panels
❌ Don't use tRPC when:
-
External clients need REST/OpenAPI (use tRPC + OpenAPI adapter)
-
Non-TypeScript clients (mobile apps, third-party integrations)
-
Microservices with different languages
Quick Start
Installation
npm install @trpc/server @trpc/client zod
For React/Next.js:
npm install @trpc/react-query @tanstack/react-query@^5
Core Setup
Always create tRPC instance in a dedicated file:
// src/server/trpc.ts import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod';
interface Context { user?: { id: string; role: string }; db: PrismaClient; }
const t = initTRPC.context<Context>().create({ errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof z.ZodError ? error.cause.flatten() : null, }, }; }, });
export const router = t.router; export const publicProcedure = t.procedure; export const middleware = t.middleware; export const createCallerFactory = t.createCallerFactory;
Procedure Patterns
Query vs Mutation
// src/server/routers/user.ts import { z } from 'zod'; import { router, publicProcedure, protectedProcedure } from '../trpc'; import { TRPCError } from '@trpc/server';
export const userRouter = router({ // Query - GET semantics (reads) getById: publicProcedure .input(z.object({ id: z.string().uuid() })) .query(async ({ input, ctx }) => { const user = await ctx.db.user.findUnique({ where: { id: input.id } }); if (!user) throw new TRPCError({ code: 'NOT_FOUND' }); return user; }),
// Mutation - POST/PUT/DELETE semantics (writes) create: protectedProcedure .input(z.object({ name: z.string().min(2).max(100), email: z.string().email(), })) .mutation(async ({ input, ctx }) => { return ctx.db.user.create({ data: input }); }), });
Cursor-Based Pagination
list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(10), cursor: z.string().uuid().optional(), })) .query(async ({ input, ctx }) => { const items = await ctx.db.user.findMany({ take: input.limit + 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: 'desc' }, });
let nextCursor: string | undefined;
if (items.length > input.limit) {
nextCursor = items.pop()?.id;
}
return { items, nextCursor };
}),
Middleware Patterns
Authentication Middleware
const isAuthed = middleware(async ({ ctx, next }) => { if (!ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { user: ctx.user } }); });
export const protectedProcedure = publicProcedure.use(isAuthed);
Role-Based Authorization
const hasRole = (role: string) => middleware(async ({ ctx, next }) => { if (ctx.user?.role !== role) { throw new TRPCError({ code: 'FORBIDDEN' }); } return next(); });
export const adminProcedure = protectedProcedure.use(hasRole('admin'));
Logging Middleware
const loggerMiddleware = middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
console.log([${type}] ${path} - ${Date.now() - start}ms);
return result;
});
Context Creation
Express Adapter
// src/server/context.ts import { CreateExpressContextOptions } from '@trpc/server/adapters/express'; import { prisma } from '../lib/prisma';
export async function createContext({ req }: CreateExpressContextOptions) { const token = req.headers.authorization?.split(' ')[1]; const user = token ? await verifyToken(token) : null;
return { user, db: prisma }; }
export type Context = Awaited<ReturnType<typeof createContext>>;
Express Server Setup
// src/server/index.ts import express from 'express'; import cors from 'cors'; import { createExpressMiddleware } from '@trpc/server/adapters/express'; import { appRouter } from './routers/_app'; import { createContext } from './context';
const app = express(); app.use(cors()); app.use('/trpc', createExpressMiddleware({ router: appRouter, createContext, }));
app.listen(3000);
Router Merging
// src/server/routers/_app.ts import { router } from '../trpc'; import { userRouter } from './user'; import { postRouter } from './post';
export const appRouter = router({ user: userRouter, post: postRouter, });
export type AppRouter = typeof appRouter;
Rules
Do ✅
-
Use Zod for all input validation
-
Create separate routers per domain and merge them
-
Use TRPCError with appropriate codes
-
Enable strict mode in TypeScript
-
Use httpBatchLink on client for request batching
-
Export AppRouter type for client
Avoid ❌
-
Mixing v10 and v11 patterns (breaking changes)
-
Skipping input validation
-
Throwing non-TRPCError exceptions (wrap them)
-
Creating multiple tRPC instances
-
Using any for context types
Error Codes Reference
Code HTTP Use Case
BAD_REQUEST
400 Invalid input
UNAUTHORIZED
401 No/invalid auth
FORBIDDEN
403 No permission
NOT_FOUND
404 Resource missing
CONFLICT
409 Already exists
INTERNAL_SERVER_ERROR
500 Unexpected error
Troubleshooting
"Types not updating on client": → Run TypeScript server in watch mode → Check AppRouter is exported and imported correctly → Verify tsconfig paths match
"Input validation errors not showing": → Add zodError to errorFormatter → Use .safeParse() on client for detailed errors
"CORS errors": → Configure cors() before tRPC middleware → Check origin whitelist
"Procedures not batching": → Ensure using httpBatchLink on client → Check all requests go to same endpoint
File Structure
src/server/ ├── trpc.ts # tRPC instance, base procedures ├── context.ts # Context creation └── routers/ ├── _app.ts # Root router (merges all) ├── user.ts # User procedures └── post.ts # Post procedures
References
-
https://trpc.io/docs — Official documentation
-
https://trpc.io/docs/client/react — React Query integration
-
https://trpc.io/docs/server/adapters — Server adapters