Technical reference
tRPC Cheatsheet
End-to-end typesafe TypeScript APIs
Must Know
typescript
Procedure
const appRouter = router({
userById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(({ input, ctx }) => ctx.db.user.findUnique({ where: { id: input.id } })),
});
export type AppRouter = typeof appRouter;tsx
Client Query
const user = trpc.userById.useQuery({ id });
if (user.isLoading) return <Spinner />;
if (user.error) return <Error message={user.error.message} />;Important Patterns
typescript
Protected Procedure
Authentication belongs in reusable middleware.
const protectedProcedure = publicProcedure.use(({ ctx, next }) => {
if (!ctx.session?.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { ...ctx, user: ctx.session.user } });
});typescript
Validate Input
Types do not replace runtime validation.
.input(z.object({
name: z.string().trim().min(1).max(100),
page: z.number().int().positive().default(1),
}))Useful Recipes
typescript
Optimistic Update
Snapshot and roll back cache state on error.
const utils = trpc.useUtils();
const mutation = trpc.todo.toggle.useMutation({
onMutate: async input => { await utils.todo.list.cancel(); /* update cache */ },
onSettled: () => utils.todo.list.invalidate(),
});typescript
Batching
Batching reduces requests but changes observability and size limits.
httpBatchLink({ url: '/api/trpc', headers: () => ({ authorization: getToken() }) })Pitfalls & Production
API Boundaries
Type safety only covers TypeScript clients using the router types.
shared inferred types
runtime input validation
stable error codes
explicit authorizationtypescript
Error Formatting
Do not leak internal stack traces or secrets.
errorFormatter({ shape, error }) {
return { ...shape, data: { ...shape.data, validation: error.cause instanceof ZodError ? error.cause.flatten() : null } };
}