What is tRPC?
tRPC lets you build fully type-safe APIs without schemas, code generation, or REST conventions. Define a function on the server, call it from the client with full TypeScript autocompletion and type checking — as if calling a local function.
Why tRPC?
- Free and open-source — MIT license
- Zero code generation — no GraphQL schemas, no OpenAPI specs
- End-to-end type safety — change server → client instantly knows
- Autocompletion — IDE shows available procedures and their types
- Framework agnostic — Next.js, React, Vue, Svelte, anything
- Subscriptions — real-time via WebSockets built in
Quick Start
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
Server (Define Your API)
// 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/users.ts
export const userRouter = router({
getAll: publicProcedure.query(async () => {
return db.select().from(users);
}),
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return db.select().from(users).where(eq(users.id, input.id));
}),
create: publicProcedure
.input(z.object({
name: z.string().min(2),
email: z.string().email()
}))
.mutation(async ({ input }) => {
return db.insert(users).values(input).returning();
}),
});
// server/index.ts
export const appRouter = router({
users: userRouter,
posts: postRouter,
});
export type AppRouter = typeof appRouter;
Client (Full Type Safety)
// React component with tRPC
import { trpc } from '../utils/trpc';
function UserList() {
// TypeScript KNOWS the return type
const { data: users, isLoading } = trpc.users.getAll.useQuery();
const createUser = trpc.users.create.useMutation({
onSuccess: () => utils.users.getAll.invalidate()
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{users?.map(user => (
<div key={user.id}>{user.name} - {user.email}</div>
))}
<button onClick={() => createUser.mutate({
name: 'Alex',
email: 'alex@example.com'
// TypeScript catches: email: 123 ← TYPE ERROR
})}>
Add User
</button>
</div>
);
}
Middleware (Auth, Logging)
const isAuthed = t.middleware(async ({ next, ctx }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({ ctx: { user: ctx.session.user } });
});
const protectedProcedure = t.procedure.use(isAuthed);
export const adminRouter = router({
getStats: protectedProcedure.query(async ({ ctx }) => {
// ctx.user is guaranteed to exist and typed
return getStatsForUser(ctx.user.id);
}),
});
Real-Time Subscriptions
// Server
const postRouter = router({
onNew: publicProcedure.subscription(() => {
return observable<Post>((emit) => {
const onAdd = (post: Post) => emit.next(post);
eventEmitter.on('newPost', onAdd);
return () => eventEmitter.off('newPost', onAdd);
});
}),
});
// Client
trpc.posts.onNew.useSubscription(undefined, {
onData(post) {
console.log('New post:', post.title);
}
});
tRPC vs Alternatives
| Feature | tRPC | REST + OpenAPI | GraphQL | Hono RPC |
|---|---|---|---|---|
| Type safety | End-to-end | Generated client | Generated types | End-to-end |
| Code generation | None | Required | Required | None |
| Learning curve | Very low | Low | High | Low |
| Over/under-fetching | No issue | Over-fetching | Solves it | No issue |
| Caching | React Query | Manual | Apollo Cache | Manual |
| Multi-language | TypeScript only | Any | Any | TypeScript only |
Real-World Impact
A team maintaining a REST API spent 30% of their time keeping OpenAPI specs and generated clients in sync. Stale types caused runtime errors in production twice a month. After migrating to tRPC: zero type mismatches, TypeScript catches breaking changes at compile time, and developers ship features 40% faster because they never wonder "what does this endpoint return?"
Building full-stack TypeScript applications? I help teams implement type-safe architectures. Contact spinov001@gmail.com or explore my data tools on Apify.
Top comments (0)