tRPC lets you build fully type-safe APIs without schemas, code generation, or runtime validation on the client. Share types directly between your TypeScript backend and frontend — zero overhead.
Why tRPC?
- Zero codegen — no OpenAPI, no GraphQL schema, no protobuf
- End-to-end types — change server code, client types update instantly
- Autocompletion — full IDE support for API calls
- Tiny — ~2KB client, zero runtime overhead
- Framework agnostic — Next.js, Express, Fastify, standalone
Server Setup
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
// Auth middleware
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { user: ctx.user } });
});
const protectedProcedure = t.procedure.use(isAuthed);
export const appRouter = router({
// Public query
hello: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello, ${input.name}!` };
}),
// List users
users: router({
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.user.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
});
return {
items: users.slice(0, input.limit),
nextCursor: users[input.limit]?.id,
};
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1),
email: z.string().email(),
}))
.mutation(async ({ input, ctx }) => {
return db.user.create({ data: { ...input, createdBy: ctx.user.id } });
}),
byId: publicProcedure
.input(z.string())
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input } });
if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
return user;
}),
}),
});
export type AppRouter = typeof appRouter;
Client (Full Autocompletion!)
// client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server/trpc';
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({ url: 'http://localhost:3000/api/trpc' }),
],
});
// Fully typed — IDE knows the exact return type!
const greeting = await trpc.hello.query({ name: 'World' });
console.log(greeting.greeting); // "Hello, World!"
// Type-safe user operations
const users = await trpc.users.list.query({ limit: 20 });
const user = await trpc.users.byId.query('user-id-123');
const newUser = await trpc.users.create.mutate({
name: 'Alice',
email: 'alice@example.com',
});
React Integration
import { trpc } from '../utils/trpc';
function UserList() {
const { data, isLoading } = trpc.users.list.useQuery({ limit: 10 });
const createUser = trpc.users.create.useMutation();
if (isLoading) return <div>Loading...</div>;
return (
<div>
{data?.items.map(user => (
<div key={user.id}>{user.name}</div>
))}
<button onClick={() => createUser.mutate({
name: 'New User',
email: 'new@example.com',
})}>
Add User
</button>
</div>
);
}
Key Features
| Feature | Details |
|---|---|
| Type safety | End-to-end, zero codegen |
| Validation | Zod, Yup, Superstruct |
| Batching | Automatic request batching |
| Subscriptions | WebSocket support |
| Frameworks | Next.js, Express, Fastify, standalone |
| React Query | Built-in integration |
Resources
Building TypeScript apps? Check my Apify actors or email spinov001@gmail.com.
Top comments (0)