Claude-skill-registry backend-trpc
Type-safe API layer for TypeScript full-stack applications. Use when building APIs that need end-to-end type safety between client and server WITHOUT code generation. Ideal for Next.js, React, and Express apps where both frontend and backend are TypeScript. Choose tRPC over REST/GraphQL when you control both ends and want zero runtime overhead for type checking.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/backend-trpc" ~/.claude/skills/majiayu000-claude-skill-registry-backend-trpc && rm -rf "$T"
skills/data/backend-trpc/SKILL.mdtRPC (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
with appropriate codesTRPCError - Enable strict mode in TypeScript
- Use
on client for request batchinghttpBatchLink - Export
type for clientAppRouter
Avoid ❌
- Mixing v10 and v11 patterns (breaking changes)
- Skipping input validation
- Throwing non-TRPCError exceptions (wrap them)
- Creating multiple tRPC instances
- Using
for context typesany
Error Codes Reference
| Code | HTTP | Use Case |
|---|---|---|
| 400 | Invalid input |
| 401 | No/invalid auth |
| 403 | No permission |
| 404 | Resource missing |
| 409 | Already exists |
| 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