tRPC: End-to-End Type Safety Without REST or GraphQL
tRPC lets you call server functions from your client as if they were local functions — fully typed, no code generation, no schema files.
If your frontend and backend are both TypeScript, tRPC eliminates an entire class of bugs: the mismatch between what your API returns and what your UI expects.
How It Works
You define procedures on the server:
// server/router.ts
import { z } from 'zod';
import { router, publicProcedure } from './trpc';
export const appRouter = router({
users: router({
list: publicProcedure
.input(z.object({ limit: z.number().default(10) }))
.query(async ({ input }) => {
return db.users.findMany({ take: input.limit });
}),
create: publicProcedure
.input(z.object({ name: z.string(), email: z.string().email() }))
.mutation(async ({ input }) => {
return db.users.create({ data: input });
}),
}),
});
export type AppRouter = typeof appRouter;
Then call them from the client with full autocomplete:
// client/UserList.tsx
import { trpc } from '../utils/trpc';
function UserList() {
// Fully typed — TS knows the shape of the response
const { data: users } = trpc.users.list.useQuery({ limit: 20 });
const createUser = trpc.users.create.useMutation();
return (
<div>
{users?.map(user => <div key={user.id}>{user.name}</div>)}
<button onClick={() => createUser.mutate({ name: 'Alice', email: 'alice@example.com' })}>
Add User
</button>
</div>
);
}
No REST endpoints. No fetch calls. No any types. If the server changes the response shape, TypeScript errors immediately in the client.
Setup
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
// client/utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../server/router';
export const trpc = createTRPCReact<AppRouter>();
Protected Procedures
Middleware lets you add authentication to procedures:
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({ ctx: { session: ctx.session } });
});
export const protectedProcedure = t.procedure.use(isAuthed);
When Not to Use tRPC
tRPC works best when:
- Both client and server are TypeScript
- They live in the same monorepo (or share types)
- You don't need a public API
If external clients (mobile apps, third-party integrations) need to consume your API, REST or GraphQL is better — tRPC doesn't have a good story for non-TypeScript consumers.
The Full Stack Picture
tRPC pairs beautifully with Next.js, Prisma, and NextAuth for a fully type-safe full-stack TypeScript app. This exact stack — plus Stripe for billing — is what powers the AI SaaS Starter Kit. Skip the setup and start building your actual product.
Top comments (0)