Building Type-Safe APIs with tRPC
Published on May 22, 2025
tRPC allows you to build fully type-safe APIs without code generation. It's perfect for TypeScript projects where you want to share types between your client and server.
What is tRPC?
tRPC stands for TypeScript Remote Procedure Call. It's a library that allows you to build APIs with type safety from end to end, meaning your client automatically knows the exact shape of data your API returns.
Setting Up tRPC
First, install the necessary packages:
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next npm install @tanstack/react-query zod
Creating Your First Router
Here's how to set up a basic tRPC router:
import { z } from 'zod';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure
.input(z.string())
.query(async ({ input }) => {
// Fetch user from database
const user = await getUserById(input);
return user;
}),
createUser: t.procedure
.input(z.object({
name: z.string().min(1),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
// Create user in database
const user = await createUser(input);
return user;
}),
});
export type AppRouter = typeof appRouter;
Client Setup
On the client side, you get full type safety:
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers/_app';
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
url: '/api/trpc',
};
},
ssr: false,
});
Using tRPC in Components
Now you can use your API with full type safety:
import { trpc } from '../utils/trpc';
export default function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading } = trpc.getUser.useQuery(userId);
const createUserMutation = trpc.createUser.useMutation();
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{user?.name}</h1>
<p>{user?.email}</p>
</div>
);
}
Advanced Features
Middleware
tRPC supports middleware for authentication, logging, and more:
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
user: ctx.user,
},
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
Subscriptions
You can also create real-time subscriptions:
export const appRouter = t.router({
onUserUpdate: t.procedure
.input(z.string())
.subscription(({ input }) => {
return observable<User>((emit) => {
// Subscribe to user updates
const unsubscribe = subscribeToUserUpdates(input, emit.next);
return unsubscribe;
});
}),
});
tRPC is an excellent choice for TypeScript projects where you want the confidence of type safety throughout your entire stack.