The Problem with REST
REST APIs force you to maintain type definitions on both client and server. When your backend schema changes, the frontend breaks silently. tRPC solves this by sharing types directly.
Setting Up tRPC with Next.js App Router
// app/api/trpc/[trpc]/route.tsimport { fetchRequestHandler } from "@trpc/server/adapters/fetch";import { appRouter } from "@/server/routers/_app";const handler = (req: Request) => fetchRequestHandler({ endpoint: "/api/trpc", req, router: appRouter, createContext: () => ({}), });export { handler as GET, handler as POST };
Defining Routers
import { z } from "zod";import { router, publicProcedure } from "../trpc";export const userRouter = router({ getById: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { return prisma.user.findUnique({ where: { id: input.id } }); }), update: publicProcedure .input(z.object({ id: z.string(), name: z.string().min(1) })) .mutation(async ({ input }) => { return prisma.user.update({ where: { id: input.id }, data: { name: input.name } }); }),});
Client-Side Usage
"use client";import { trpc } from "@/utils/trpc";export function UserProfile({ userId }: { userId: string }) { const { data: user, isLoading } = trpc.user.getById.useQuery({ id: userId }); if (isLoading) return <Spinner />; return <div>{user?.name}</div>;}
Key Benefits
Zero API contract drift — types are shared automatically
Full autocomplete for API calls in your IDE
Automatic input validation with Zod
Built-in error handling
Subscriptions for real-time data