Why tRPC?
In a Next.js monorepo where the client and server are in the same repository and both written in TypeScript, maintaining separate REST API types is busywork. tRPC lets your React components call server functions with full type safety — no code generation, no schema files, no fetch wrappers to maintain.
Setting Up tRPC v11 in Next.js 15
pnpm add @trpc/server@11 @trpc/client@11 @trpc/react-query@11 @tanstack/react-query@5
// server/trpc.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
Defining Procedures
// server/routers/tasks.ts
import { router, publicProcedure } from "../trpc";
import { z } from "zod";
import { db } from "@/lib/db";
export const tasksRouter = router({
list: publicProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input }) => {
return db.tasks.findMany({ where: { projectId: input.projectId } });
}),
create: publicProcedure
.input(z.object({ title: z.string().min(1), projectId: z.string() }))
.mutation(async ({ input }) => {
return db.tasks.create({ data: input });
}),
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
await db.tasks.delete({ where: { id: input.id } });
return { ok: true };
}),
});
// server/root.ts
import { router } from "./trpc";
import { tasksRouter } from "./routers/tasks";
export const appRouter = router({ tasks: tasksRouter });
export type AppRouter = typeof appRouter;
Next.js App Router Handler
// 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: () => ({}),
});
export { handler as GET, handler as POST };
Using tRPC in React Components
// components/TaskList.tsx
"use client";
import { trpc } from "@/lib/trpc/client";
export function TaskList({ projectId }: { projectId: string }) {
const { data: tasks, isLoading } = trpc.tasks.list.useQuery({ projectId });
const createTask = trpc.tasks.create.useMutation({
onSuccess: () => utils.tasks.list.invalidate(),
});
const utils = trpc.useUtils();
if (isLoading) return <div>Loading...</div>;
return (
<div>
{tasks?.map(task => <div key={task.id}>{task.title}</div>)}
<button onClick={() => createTask.mutate({ title: "New task", projectId })}>
Add Task
</button>
</div>
);
}
Every type is inferred from the server definition — if you rename a field in the database, TypeScript will flag every component that uses it.
v11 New Features
TanStack Query v5 adapter — the React hooks now use TQ v5's unified API:
// Old (v10 with TQ v4)
const { data } = trpc.tasks.list.useQuery(input, { keepPreviousData: true });
// New (v11 with TQ v5)
const { data } = trpc.tasks.list.useQuery(input, { placeholderData: "keepPrevious" });
Streaming via SSE — for AI-style streaming responses:
streamProcedure: publicProcedure.subscription(async function* () {
for await (const chunk of aiStream) {
yield chunk;
}
});
When to Use tRPC vs REST
Use tRPC when: you have a Next.js monorepo, all clients are TypeScript, and you want maximum development speed.
Use REST when: you need a public API, have non-TypeScript clients (mobile apps, third parties), or want to be decoupled from the Node.js ecosystem.
References: tRPC · GitHub · Next.js integration