DM
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 authorization
typescript

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 } };
}