tRPC eliminates the API contract layer — the TypeScript type of a server procedure is the contract, inferred automatically on the client. t.procedure.input(z.object({...})).query(async ({ input, ctx }) => {...}) defines a fully typed endpoint. Zod validates input at runtime. ctx carries authentication context threaded by middleware. The client calls trpc.orders.list.useQuery({ customerId }) — the return type, input shape, and error types all infer from the server definition without generating code or schema files. tRPC v11 introduces streamlined syntax, better Next.js App Router integration via @trpc/next, and native support for React Query v5’s useSuspenseQuery. Subscriptions stream updates via server-sent events. Claude Code generates tRPC routers, procedure definitions, context setup, middleware chains, React Query hooks, and the Next.js App Router integration for production tRPC applications.
CLAUDE.md for tRPC v11
## tRPC v11 Stack
- Version: @trpc/server >= 11, @trpc/client >= 11, @trpc/react-query >= 11
- Server: initTRPC.context<Context>().create() → t.router({ ... })
- Procedures: t.procedure.input(ZodSchema).query/mutation(handler)
- Context: createTRPCContext in separate file — injected per request
- Middleware: t.procedure.use(authMiddleware) — chain with .use()
- Client: createTRPCReact<AppRouter>() — typed hooks from router
- Next.js: fetchRequestHandler in app/api/trpc/[trpc]/route.ts
Router and Procedures
// server/trpc/router/orders.ts — tRPC order router
import { TRPCError } from "@trpc/server"
import { z } from "zod"
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../init"
import { db } from "@/server/db"
const CreateOrderInput = z.object({
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive().max(100),
priceCents: z.number().int().positive(),
})).min(1, "At least one item required"),
shippingAddress: z.object({
line1: z.string().min(1),
city: z.string().min(1),
country: z.string().length(2),
postalCode: z.string().min(1),
}),
})
export const ordersRouter = createTRPCRouter({
// Public query — no auth required
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input }) => {
const order = await db.orders.findUnique({
where: { id: input.id },
include: { items: true },
})
if (!order) throw new TRPCError({ code: "NOT_FOUND", message: "Order not found" })
return order
}),
// Protected query — requires auth (via middleware)
list: protectedProcedure
.input(z.object({
status: z.enum(["pending", "processing", "shipped", "delivered", "cancelled"]).optional(),
cursor: z.string().optional(),
limit: z.number().int().min(1).max(100).default(20),
}))
.query(async ({ input, ctx }) => {
const { status, cursor, limit } = input
const orders = await db.orders.findMany({
where: {
customerId: ctx.user.id, // auth.uid from context
...(status ? { status } : {}),
},
take: limit + 1,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: "desc" },
})
let nextCursor: string | undefined
if (orders.length > limit) {
const nextItem = orders.pop()
nextCursor = nextItem?.id
}
return { orders, nextCursor }
}),
// Protected mutation
create: protectedProcedure
.input(CreateOrderInput)
.mutation(async ({ input, ctx }) => {
const totalCents = input.items.reduce(
(sum, item) => sum + item.priceCents * item.quantity,
0
)
const order = await db.orders.create({
data: {
customerId: ctx.user.id,
status: "pending",
totalCents,
items: { create: input.items },
shippingAddress: input.shippingAddress,
},
include: { items: true },
})
return order
}),
cancel: protectedProcedure
.input(z.object({ id: z.string().uuid() }))
.mutation(async ({ input, ctx }) => {
const order = await db.orders.findUnique({ where: { id: input.id } })
if (!order) throw new TRPCError({ code: "NOT_FOUND" })
if (order.customerId !== ctx.user.id) {
throw new TRPCError({ code: "FORBIDDEN" })
}
if (order.status !== "pending") {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Cannot cancel order with status: ${order.status}`,
})
}
return db.orders.update({
where: { id: input.id },
data: { status: "cancelled" },
})
}),
})
tRPC Init and Root Router
// server/trpc/init.ts — tRPC initialization
import { initTRPC, TRPCError } from "@trpc/server"
import { cache } from "react"
import { auth } from "@/server/auth"
import superjson from "superjson"
export type TRPCContext = {
user: { id: string; email: string } | null
}
// Context factory — runs per request
export const createTRPCContext = cache(async () => {
const session = await auth()
return {
user: session?.user ?? null,
} satisfies TRPCContext
})
const t = initTRPC.context<TRPCContext>().create({
transformer: superjson, // Serialize Dates, Maps, Sets
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
}
},
})
export const createTRPCRouter = t.router
export const publicProcedure = t.procedure
// Auth middleware
const enforceAuth = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" })
}
return next({ ctx: { ...ctx, user: ctx.user } })
})
export const protectedProcedure = t.procedure.use(enforceAuth)
// Admin middleware
const enforceAdmin = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" })
const user = await db.users.findUnique({ where: { id: ctx.user.id } })
if (user?.role !== "admin") throw new TRPCError({ code: "FORBIDDEN" })
return next({ ctx: { ...ctx, user: { ...ctx.user, role: "admin" as const } } })
})
export const adminProcedure = t.procedure.use(enforceAdmin)
// server/trpc/root.ts — root router combining all sub-routers
import { createTRPCRouter } from "./init"
import { ordersRouter } from "./router/orders"
import { productsRouter } from "./router/products"
import { usersRouter } from "./router/users"
export const appRouter = createTRPCRouter({
orders: ordersRouter,
products: productsRouter,
users: usersRouter,
})
export type AppRouter = typeof appRouter
Next.js App Router Handler
// app/api/trpc/[trpc]/route.ts — Next.js App Router tRPC handler
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"
import { appRouter } from "@/server/trpc/root"
import { createTRPCContext } from "@/server/trpc/init"
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: createTRPCContext,
onError: ({ error, path }) => {
if (error.code === "INTERNAL_SERVER_ERROR") {
console.error(`tRPC error on ${path}:`, error)
}
},
})
export { handler as GET, handler as POST }
React Client Integration
// src/trpc/client.ts — typed React hooks setup
"use client"
import { createTRPCReact } from "@trpc/react-query"
import type { AppRouter } from "@/server/trpc/root"
export const trpc = createTRPCReact<AppRouter>()
// src/components/OrderList.tsx — using tRPC React hooks
"use client"
import { trpc } from "@/trpc/client"
import { useSuspenseQuery } from "@tanstack/react-query"
export function OrderList() {
// Fully typed — input and return type inferred from server
const [data, { fetchNextPage, hasNextPage }] = trpc.orders.list.useSuspenseInfiniteQuery(
{ limit: 20 },
{ getNextPageParam: (lastPage) => lastPage.nextCursor }
)
const cancelMutation = trpc.orders.cancel.useMutation({
onSuccess: () => {
trpc.useUtils().orders.list.invalidate()
},
onError: (error) => {
console.error(error.message)
},
})
const orders = data.pages.flatMap(p => p.orders)
return (
<div>
{orders.map(order => (
<div key={order.id}>
<span>{order.id.slice(-8)}</span>
<span>{order.status}</span>
<span>${(order.totalCents / 100).toFixed(2)}</span>
{order.status === "pending" && (
<button
onClick={() => cancelMutation.mutate({ id: order.id })}
disabled={cancelMutation.isPending}
>
Cancel
</button>
)}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>Load more</button>
)}
</div>
)
}
For the Hono RPC alternative that provides similar TypeScript-inferred client types using Hono’s RPC syntax without the React Query dependency, see the edge API patterns. For the GraphQL alternative when consumers beyond web apps (mobile, third parties) need a self-documenting, flexible query language instead of tRPC’s TypeScript-first model, the GraphQL guide covers schema definition and resolvers. The Claude Skills 360 bundle includes tRPC v11 skill sets covering router setup, middleware chains, and React integration. Start with the free tier to try tRPC router generation.