Back to Blog

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.