The first thing that catches you off guard with tRPC is the moment you tweak a single type and suddenly seven other files light up red in your editor. I hit this wall myself when I introduced tRPC v11 into a personal project — a small change to appRouter wiped out autocompletion on the client, and my IDE became a sea of squiggly underlines. What rescued me was Claude Code's ability to read multiple related files at once and trace the dependency chain.
This article shares a concrete workflow for using Claude Code with tRPC to resolve type errors quickly and to preserve type safety from the design stage onward. I've focused on the pain points that official docs rarely surface — the ones you only learn by shipping.
Why tRPC Pairs So Well with Claude Code
The real value of tRPC is that the moment you change a router definition on the server, the client's type completion updates in sync. The flip side is that this chain reaction makes type errors spread fast, and tracking down the root cause means hopping between multiple files.
Claude Code is particularly strong at exactly this kind of problem because it reads multiple files in parallel and follows the dependency graph. In my experience, type errors that used to take me thirty minutes of patient detective work now resolve in about five.
The three scenarios where this shines most are:
- Router refactoring: When you move a procedure to a different router, Claude Code rewrites every caller's type reference in one pass
- Input schema changes: Modifying a Zod schema propagates through to the prop types of React components that consume the query
- Middleware type composition: When you change the context type of
createTRPCRouter, it updates every auth-related procedure consistently
Setting Up a Minimal Project
Let's get something running first. For a Next.js 15 App Router project with tRPC v11, the minimal setup is:
# Install dependencies
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
npm install -D typescript @types/nodeI've found the cleanest layout is to put server routers at src/server/routers/_app.ts and centralize client hooks in src/lib/trpc.ts.
// src/server/routers/_app.ts
import { z } from "zod";
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure
.input(z.object({ name: z.string().min(1) }))
.query(({ input }) => {
return { message: `Hello, ${input.name}` };
}),
});
// Export the type only — never export the implementation
export type AppRouter = typeof appRouter;The final line is the key. Only the AppRouter type flows to the client; implementation code (like database access) never ends up in the client bundle. This "ship only types" design is the heart of tRPC, and understanding it makes decoding later type errors much easier.
Deciding What to Hand Off to Claude Code
This is an important judgment call. If you hand the entire router design to an AI from day one, procedure granularity ends up inconsistent and maintenance suffers. Here's how I split the work:
What the human owns:
- Router partitioning strategy (the boundaries between
userRouter,postRouter, etc.) - Authentication and authorization policy
- Error handling conventions
What Claude Code handles:
- Keeping Zod schemas and TypeScript types in sync
- Writing the client-side hook calls
- Generating test case scaffolding
A concrete prompt looks like this:
Please add the following to src/server/routers/user.ts:
- createUser: input { email: string, name: string }, output { id: string }
- getUser: input { id: string }, output User or null
Use Zod for validation. Only allow RFC-compliant email addresses.
Implementation can be a stub that console.logs — I'll wire up the DB later.
Humans decide the design; the AI handles type plumbing and validation. This division of labor is what actually holds up in practice.
A Debugging Workflow That Dissolves Type Errors Fast
This is the heart of the article. Most tRPC type errors fall into three patterns.
Pattern 1: "This procedure doesn't exist"
You call trpc.user.create.useMutation() on the client and TypeScript yells "Property 'create' does not exist." The cause is almost always that you forgot to register userRouter on appRouter, or the export path is wrong.
How to ask Claude Code:
Getting a type error on trpc.user.create in src/lib/trpc.ts.
Please check src/server/routers/_app.ts and src/server/routers/user.ts
to see if the user router is registered correctly.
Claude Code opens both files and spots the missing entry in t.router({ ... }) almost instantly.
Pattern 2: "Input types don't match"
You changed a Zod schema, and every call site on the client is now broken. This requires hunting down every caller.
I changed the input schema for createPost in src/server/routers/post.ts
from { title: string, body: string } to { title: string, body: string, tags: string[] }.
Please find every call site and pass tags. An empty array [] is fine for now.
Claude Code runs a grep-style search, finds every match, and proposes the diffs.
Pattern 3: "Server and client types are out of sync"
The export type AppRouter on the server is correct, but the client still sees the old version because the build cache is stale. This is more of an environment issue than a real type error. Try these in order:
- Delete the
.nextfolder - Delete the TypeScript build cache (
tsconfig.tsbuildinfo) - Restart the TypeScript server in your IDE
- If none of that works, clear
node_modules/.cache
The pragmatic move is to teach Claude Code this sequence once by noting it in project memory (CLAUDE.md). Next time the symptom appears, it'll run the steps for you without prompting.
Pitfalls I Actually Hit
I've painted a clean picture so far, but the field is messier. Here are three specific pitfalls from my own projects.
Middleware types silently falling to any: When I tried to infer the context type on createTRPCRouter, the middleware return type resolved to any. The culprit was omitting the type argument on t.middleware. Writing t.middleware<{ userId: string }>(async ({ ctx, next }) => ...) explicitly fixed it.
Zod's .optional() vs TypeScript's ? mismatch: z.object({ name: z.string().optional() }) produces { name?: string | undefined }. Passing { name: "" } from the client passes validation, but whether your server should accept empty strings is a separate business decision. Don't let the type signature lull you into skipping real validation.
inferQueryInput becoming unwieldy: Referencing procedure input types directly — inferRouterInputs<AppRouter>["user"]["getUser"] — makes code hard to read. I now collect type aliases in src/lib/trpc-types.ts and reference them as GetUserInput or similar short names everywhere.
A Next Step You Can Take Today
If you're on the fence about adopting tRPC, start by replacing one procedure in an existing Next.js project. Swapping a single api/hello endpoint over to tRPC is enough to feel the type completion quality. From there, expand gradually. This is the only introduction path I've seen that doesn't end in a failed rollback.
When pairing tRPC with Claude Code, add a note to the project's CLAUDE.md saying something like: "This project uses tRPC v11. Routers live under src/server/routers/. Every new procedure must define a Zod schema." The suggestion quality from that point forward improves noticeably.
When tRPC Is Not the Right Choice
I want to be honest here: tRPC is not a universal answer. A few situations where I would reach for something else:
-
Public APIs consumed by unknown clients: Since tRPC ties the client to the server's TypeScript types, it only pays off when both ends share a codebase. If you are exposing an API to external partners, use OpenAPI or GraphQL.
-
Heavy binary payloads: File uploads, streaming, and binary formats fit awkwardly into tRPC's request/response model. A plain REST endpoint with
multipart/form-datais cleaner for these cases. -
Teams with polyglot clients: If your mobile app is Kotlin or Swift and your frontend is TypeScript, the type sharing story falls apart outside the TypeScript world.
The honest sweet spot is a TypeScript monorepo where the same engineers ship both server and client. For those projects, the productivity gain is real.
One More Claude Code Habit Worth Adopting
When reviewing Claude Code's suggestions for tRPC code, I always ask it to explain its choice of inference style before I accept the diff. A quick "why did you use inferRouterOutputs here instead of typing it manually?" surfaces the model's reasoning and catches subtle mistakes early. The few extra seconds this takes has saved me multiple debugging sessions.
For readers who want to go deeper on TypeScript's type system itself, Matt Pocock's TypeScript content (both his free material and the Total TypeScript courses) is excellent preparation for reading libraries like tRPC that lean heavily on advanced inference.
If you want to push Claude Code's hooks and MCP integration further, the Claude Code Hooks Automation Master Guide walks through pre- and post-check patterns. Automating type generation on tRPC router changes pairs especially well with that setup.