DEV Community

Atlas Whoff
Atlas Whoff

Posted on

tRPC: End-to-End Type Safety Between Next.js and Node.js

tRPC: End-to-End Type Safety Between Next.js and Node.js

REST APIs break type safety at the network boundary. Every API call requires manual type assertions that diverge the moment someone changes the backend. tRPC solves this by sharing TypeScript types across client and server.

What tRPC Does

Without tRPC: frontend fetch('/api/user') returns any — you cast it and hope.

With tRPC: frontend trpc.user.getById.query({ id: '123' }) is fully typed — autocomplete, compile-time errors, and refactoring all work across the boundary.

Installation

npm install @trpc/server @trpc/client @trpc/next @trpc/react-query @tanstack/react-query zod
Enter fullscreen mode Exit fullscreen mode

Server Setup

// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { getServerSession } from 'next-auth';
import { z } from 'zod';

const t = initTRPC.context<{ session: Session | null }>().create();

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
  if (!ctx.session) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next({ ctx: { session: ctx.session } });
});
Enter fullscreen mode Exit fullscreen mode

Defining Procedures

// server/routers/user.ts
export const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return prisma.user.findUnique({ where: { id: input.id } });
    }),

  update: protectedProcedure
    .input(z.object({
      name: z.string().min(1).max(100),
      bio: z.string().max(500).optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      return prisma.user.update({
        where: { id: ctx.session.user.id },
        data: input,
      });
    }),
});

// server/root.ts
export const appRouter = router({ user: userRouter });
export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode

Next.js API Route

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/root';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: async () => ({
      session: await getServerSession(),
    }),
  });

export { handler as GET, handler as POST };
Enter fullscreen mode Exit fullscreen mode

Client Usage

// In any React component
import { trpc } from '@/utils/trpc';

function UserProfile({ userId }: { userId: string }) {
  // Fully typed — hover to see the return type
  const { data: user, isLoading } = trpc.user.getById.useQuery({ id: userId });

  const updateUser = trpc.user.update.useMutation({
    onSuccess: () => utils.user.getById.invalidate({ id: userId }),
  });

  return (
    <form onSubmit={() => updateUser.mutate({ name: 'New Name' })}>
      {user?.name}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Server Component Support

// In a React Server Component
import { createCaller } from '@/server/root';

export default async function Page() {
  const caller = createCaller({ session: await getServerSession() });
  const user = await caller.user.getById({ id: '123' }); // Fully typed
  return <div>{user?.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

tRPC ships pre-configured in the AI SaaS Starter Kit — router, context, auth middleware, and client all wired up. $99 one-time at whoffagents.com.

Top comments (0)