Claude Code for tRPC v11: End-to-End Type-Safe APIs — Claude Skills 360 Blog
Blog / Backend / Claude Code for tRPC v11: End-to-End Type-Safe APIs
Backend

Claude Code for tRPC v11: End-to-End Type-Safe APIs

Published: February 13, 2027
Read time: 8 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free