The API Problem
REST: you guess the response shape. GraphQL: you write schemas, resolvers, and run codegen. Both: your frontend and backend types drift apart.
tRPC shares types directly between server and client. Change a return type on the server → your frontend gets a type error instantly. No codegen. No schemas.
What tRPC Gives You
Define a Router
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const user = await db.users.findById(input.id);
return user; // TypeScript knows the exact shape
}),
createPost: t.procedure
.input(z.object({
title: z.string().min(1),
content: z.string(),
}))
.mutation(async ({ input }) => {
return db.posts.create(input);
}),
});
export type AppRouter = typeof appRouter;
Call From the Client
import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from '../server';
const trpc = createTRPCClient<AppRouter>({ /* config */ });
// Full autocomplete + type checking
const user = await trpc.getUser.query({ id: '123' });
console.log(user.name); // TypeScript knows this exists
const post = await trpc.createPost.mutate({
title: 'Hello',
content: 'World',
});
React Integration
function UserProfile({ id }: { id: string }) {
const { data, isLoading } = trpc.getUser.useQuery({ id });
if (isLoading) return <p>Loading...</p>;
return <h1>{data?.name}</h1>; // Fully typed
}
Subscriptions (WebSockets)
// Server
onNewMessage: t.procedure.subscription(() => {
return observable<Message>((emit) => {
const onMessage = (msg: Message) => emit.next(msg);
ee.on('message', onMessage);
return () => ee.off('message', onMessage);
});
}),
// Client
trpc.onNewMessage.subscribe(undefined, {
onData(message) {
console.log('New message:', message);
},
});
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);
Works With
Next.js, Remix, Nuxt, SvelteKit, Express, Fastify, Bun, Deno — any TypeScript stack.
Why This Matters
If your frontend and backend are both TypeScript, you don't need REST or GraphQL. tRPC gives you a function call that happens to cross the network — with full type safety.
Building type-safe APIs that need web data? Check out my web scraping actors on Apify Store — structured data via API. For custom solutions, email spinov001@gmail.com.
Top comments (0)