trpc-pro
tRPC guidance — end-to-end typesafe APIs, routers, procedures, auth, and React Query integration.
Files1
View on GitHubSKILL.mdskills/trpc-pro/SKILL.md
GitHubOverview
tRPC gives you end-to-end type safety for TypeScript APIs without codegen or schemas: define procedures on the server, call them from the client like functions, and types flow automatically. It's the fastest path to a typesafe full-stack TypeScript app — and it's deliberately not a public-API technology. This skill covers structuring tRPC routers, procedure patterns (query/mutation/subscription), auth via context, and the React Query integration that powers the client.
When to use
- Building a full-stack TypeScript app (Next.js, Vite + Express, etc.).
- Choosing between tRPC, REST, and GraphQL for internal APIs.
- Structuring routers, procedures, and middleware in tRPC.
- Adding auth, validation (Zod), and error handling to tRPC.
- Using subscriptions (WebSockets/SSE) with tRPC.
- Migrating an existing REST API to tRPC incrementally.
Core concepts
- Procedures are the API.
publicProcedure,protectedProcedure(via middleware),query/mutation/subscription— each procedure is one endpoint with inferred input/output types. No schema files, no codegen step. - Routers compose. Nest routers by domain (
appRouter→userRouter,orderRouter) withmergeRoutersor nested.router()calls. Router structure is your API structure — organize by domain, not by technical layer. - Context is the request scope. Created per request (user, DB client, request ID); middleware enriches or rejects it. Auth checks belong in middleware producing
protectedProcedure— not repeated inside every procedure. - Validation with Zod.
.input(z.object({...}))validates at runtime and infers TypeScript types. Validate every input; tRPC's type safety is compile-time only without it. - Middleware pipeline. Reusable wrappers for auth, logging, rate limiting:
.use()chains that run before the procedure. Compose small middlewares rather than one god-middleware. - Error shaping.
TRPCErrorwith codes (UNAUTHORIZED,NOT_FOUND,BAD_REQUEST...) maps to HTTP statuses; unexpected errors masked in production. Keep error codes consistent — clients switch on them. - React Query integration. The client is a thin wrapper over TanStack Query:
trpc.order.get.useQuery(), mutations with optimistic updates, automatic cache invalidation viautils. Learn invalidation patterns — stale UI is the common complaint. - Batching.
httpBatchLinkcombines calls into one HTTP request — fewer round trips, but one slow procedure delays the batch; split latency-sensitive calls onto a separate link. - Subscriptions. WebSocket or SSE links for realtime; like all subscriptions, design for reconnect and resume. tRPC subscriptions are convenient but carry the same operational weight as raw WebSockets.
- It's not for public APIs. tRPC assumes a TypeScript client and couples client/server deploys. For third-party consumers, put a REST (or GraphQL) facade in front — don't expose tRPC directly.
- Versioning. tRPC has no built-in versioning; evolve procedures additively, deprecate with naming or comments, and coordinate client/server deploys since types are shared.
- Server-component caller.
createCallerinvokes procedures directly inside React Server Components without HTTP — same validation and auth, zero round trip. - Output validation.
.output(zodSchema)validates procedure results at runtime — catches service-layer shape drift before it reaches clients.
Practical workflow
- Set up the foundation.
initTRPCwith context creation (auth session, DB), apublicProcedurebase, and middleware for logging; choose links (httpBatchLinkdefault,wsLinkif subscriptions).export const t = initTRPC.context<Context>().create(); export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(isAuthed); - Build domain routers. One router per domain with queries/mutations; Zod schemas for inputs; thin procedures calling services.
export const orderRouter = t.router({ get: protectedProcedure .input(z.object({ id: z.string().uuid() })) .query(({ ctx, input }) => orderService.get(ctx, input.id)), cancel: protectedProcedure .input(z.object({ id: z.string().uuid() })) .mutation(({ ctx, input }) => orderService.cancel(ctx, input.id)), }); - Enforce auth in middleware.
isAuthedthrowsUNAUTHORIZEDwhenctx.useris missing; resource-level checks (ownership) inside services, not routers. - Handle errors consistently. Throw
TRPCErrorwith the right code; global error formatter for logging + masking; clients map codes to UI states. - Wire the client. React Query provider + tRPC client; use
useQuery/useMutationwithonSuccessinvalidation (utils.order.get.invalidate()); optimistic updates for snappy mutations with rollback on error.const utils = trpc.useUtils(); const cancel = trpc.order.cancel.useMutation({ onMutate: async (input) => { await utils.order.get.cancel(); const prev = utils.order.get.getData({ id: input.id }); utils.order.get.setData({ id: input.id }, (o) => o && { ...o, status: "cancelled" }); return { prev }; }, onError: (_e, input, ctx) => utils.order.get.setData({ id: input.id }, ctx?.prev), onSettled: (_d, _e, input) => utils.order.get.invalidate({ id: input.id }), }); - Paginate inputs. Cursor-based input schemas (
{ cursor, limit }) for lists; never return unbounded arrays from a query. - Add subscriptions where needed.
wsLink+ subscription procedures for live data; client reconnect logic; server-side cleanup on disconnect. - Deploy coordinated. Since types are shared, deploy server and client together (monorepo) or version the router contract; monitor procedure-level error rates and latencies.
Common pitfalls
- No input validation — trusting TypeScript types at runtime; every procedure needs a Zod schema.
- Auth inside procedures — repeated, inconsistent checks; centralize in middleware →
protectedProcedure. - Fat procedures — business logic in routers becomes untestable; push to services.
- Unbounded queries — returning full tables; paginate every list procedure.
- Over-batching latency — one slow procedure in a batch delays everything; split links by latency class.
- Exposing tRPC publicly — third parties can't consume it without TypeScript; add a REST facade for external consumers.
- Ignoring invalidation — mutations that don't invalidate queries leave stale UI; define invalidation per mutation.
- Leaking internal errors — raw DB/validation errors to clients; format and mask in production.
- No rate limiting — procedures are cheap to call in loops; add middleware-based limits on expensive ones.
- Breaking changes without coordination — renaming a procedure breaks the client build; treat router changes as contract changes in CI.
- Skipping output validation — service refactors silently changing response shapes;
.output()schemas catch the drift in tests. - Server-component waterfalls — awaiting procedures sequentially in RSCs; parallelize with
Promise.allor prefetch instead.