REST needs schemas, validators, and code generation for type safety. GraphQL needs schemas, resolvers, and code generation for type safety. tRPC gives you end-to-end type safety with zero code generation, zero schemas, and zero boilerplate.
What tRPC Gives You for Free
- End-to-end type safety — change a server function, client gets type errors instantly
- Zero code generation — types flow automatically from server to client
- No schema files — your TypeScript IS the schema
- Subscriptions — real-time updates via WebSocket
- Batching — multiple requests in one HTTP call
- Works with Next.js, SvelteKit, Remix, Express, Fastify, SolidStart
Quick Start
npm install @trpc/server @trpc/client @trpc/react-query
Server Setup
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
// server/routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const user = await db.users.find(input.id);
return user; // TypeScript infers the return type
}),
create: publicProcedure
.input(z.object({
name: z.string().min(2),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
return await db.users.create({ data: input });
}),
list: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
}))
.query(async ({ input }) => {
const users = await db.users.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
});
return {
items: users.slice(0, input.limit),
nextCursor: users[input.limit]?.id,
};
}),
});
Client Usage (Fully Typed)
// React component
import { trpc } from '../utils/trpc';
export function UserProfile({ userId }: { userId: string }) {
// TypeScript knows: data is { id, name, email, ... } | undefined
const { data: user, isLoading } = trpc.user.getById.useQuery({ id: userId });
// TypeScript knows: mutate takes { name: string, email: string }
const createUser = trpc.user.create.useMutation();
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{user?.name}</h1>
<p>{user?.email}</p>
<button onClick={() => createUser.mutate({ name: 'New', email: 'new@test.com' })}>
Create User
</button>
</div>
);
}
Change the server return type → client immediately shows type errors. No code generation needed.
Middleware (Authentication, Logging)
const isAuthed = t.middleware(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({ ctx: { user: ctx.session.user } });
});
const protectedProcedure = t.procedure.use(isAuthed);
// Now ctx.user is typed and guaranteed to exist
export const settingsRouter = router({
update: protectedProcedure
.input(z.object({ theme: z.enum(['light', 'dark']) }))
.mutation(async ({ ctx, input }) => {
return await db.settings.update({
where: { userId: ctx.user.id }, // ctx.user is typed!
data: { theme: input.theme }
});
})
});
tRPC vs REST vs GraphQL
| Feature | tRPC | REST | GraphQL |
|---|---|---|---|
| Type safety | Automatic | Manual (codegen) | Manual (codegen) |
| Schema files | None | OpenAPI spec | .graphql files |
| Code generation | None | Required for types | Required for types |
| Overfetching | N/A (RPC) | Common | Solved |
| Learning curve | Know TS → ready | Low | High |
| Best for | TS full-stack apps | Public APIs | Complex data graphs |
The Verdict
tRPC is the fastest path to a type-safe full-stack TypeScript application. No schemas, no codegen, no boilerplate — just TypeScript functions on the server that the client calls with full type inference.
Need help building production web scrapers or data pipelines? I build custom solutions. Reach out: spinov001@gmail.com
Check out my awesome-web-scraping collection — 400+ tools for extracting web data.
Top comments (0)