tRPC is a type-safe RPC framework that lets TypeScript clients call server functions as if they were local — with end-to-end type inference and no code generation. AI tools that treat it like REST or GraphQL miss the entire point.
The most common failure modes: defining routers like REST controllers, breaking the type inference chain with any or manual type casts, adding unnecessary serialization, and not using the built-in React Query integration for client-side data fetching.
A CLAUDE.md file that declares the tRPC model prevents these patterns. Here are 13 rules.
Rule 1: Procedures, not endpoints
tRPC uses procedures (query, mutation, subscription), not HTTP endpoints.
Do NOT name procedures like REST resources (getUserById, createUser).
Do NOT add route prefixes or HTTP method annotations.
Procedures are typed functions — name them as verbs or descriptors:
user.getById, user.create, post.list, auth.login
Rule 2: End-to-end type inference must be preserved
The type inference chain from server to client must never be broken.
Do NOT use 'any', 'unknown' without narrowing, or explicit type casts
in router definitions, middleware, or context.
The AppRouter type export from the server is the single source of truth.
Client types derive automatically — do not duplicate them.
Rule 3: Input validation uses Zod
All procedure inputs are validated with Zod schemas:
.input(z.object({ id: z.string(), name: z.string().min(1) }))
Do NOT validate inputs manually in the procedure handler.
Zod schemas serve as both runtime validation and TypeScript type source.
Rule 4: Context carries authentication and shared dependencies
Authentication, database connections, and request context belong in
the createContext function, not in individual procedures.
Middleware (authedProcedure, adminProcedure) extends the base procedure
to add typed context fields.
Do NOT access req/res directly inside procedure handlers.
Rule 5: The React client uses tRPC hooks, not fetch
Client-side data fetching uses tRPC's React Query integration:
trpc.user.getById.useQuery({ id })
trpc.user.create.useMutation()
Do NOT use fetch(), axios, or raw useEffect for tRPC calls.
The hook names mirror the router structure exactly.
Rule 6: Router structure is the API contract
The router tree defines the namespace:
appRouter = router({
user: userRouter,
post: postRouter,
auth: authRouter,
})
Do NOT flatten all procedures into a single root router.
Sub-routers group related procedures — mirror the domain model.
Rule 7: Error handling uses TRPCError
Throw TRPCError for expected errors:
throw new TRPCError({ code: 'NOT_FOUND', message: '...' })
Error codes map to HTTP status codes automatically.
Do NOT throw plain Error objects or return { success: false } objects.
Use the code field: NOT_FOUND, UNAUTHORIZED, BAD_REQUEST, INTERNAL_SERVER_ERROR
Rule 8: Subscriptions use observable or async generators
Real-time procedures use .subscription() with observable or async generator:
.subscription(() => observable((emit) => { ... }))
Do NOT implement polling as a workaround for subscriptions.
WebSocket transport is required for subscriptions.
Rule 9: Middleware is reusable typed context extension
Shared behavior (auth checks, rate limiting, logging) uses middleware:
const authedProcedure = publicProcedure.use(({ ctx, next }) => {
if (!ctx.session) throw new TRPCError({ code: 'UNAUTHORIZED' })
return next({ ctx: { ...ctx, user: ctx.session.user } })
})
Do NOT copy auth checks into individual procedure handlers.
Rule 10: Output validation is optional but explicit
Output types are inferred automatically from the return type.
Use .output(schema) only when explicit runtime validation is required.
Do NOT add .output() to every procedure — it doubles the validation overhead.
Add .output() when the return data comes from an untrusted source.
Rule 11: Links configure the client transport
The tRPC client is configured with links:
httpBatchLink for standard HTTP (batches multiple calls)
wsLink for WebSocket subscriptions
splitLink for mixed HTTP + WebSocket
Do NOT configure fetch directly — use the link layer.
Rule 12: Server-side calls use createCaller
To call tRPC procedures from server-side code (SSR, cron jobs, tests):
const caller = appRouter.createCaller(context)
const result = await caller.user.getById({ id })
Do NOT expose internal procedure handlers as standalone functions.
createCaller preserves the middleware and context chain.
Rule 13: File structure mirrors the router tree
Router files should mirror the namespace:
server/routers/user.ts → userRouter
server/routers/post.ts → postRouter
server/trpc.ts → base procedure, context, middleware
server/root.ts → appRouter assembly
Do NOT define all procedures in a single file.
The full CLAUDE.md block
# tRPC Project Rules
## Core Model
- Procedures (query/mutation/subscription), not HTTP endpoints
- End-to-end type inference must be preserved — no 'any' in router code
- AppRouter type is the single source of truth for client types
## Input Validation
- All inputs validated with Zod schemas via .input()
- No manual validation in procedure handlers
## Client
- React Query integration: trpc.[namespace].[procedure].useQuery/useMutation
- No fetch(), axios, or raw useEffect for tRPC calls
## Context & Auth
- Auth and shared deps in createContext
- Middleware (authedProcedure, adminProcedure) for access control
- No direct req/res access in procedure handlers
## Errors
- Throw TRPCError with code field (NOT_FOUND, UNAUTHORIZED, etc.)
- No plain Error objects or { success: false } return patterns
## Router Structure
- Nested sub-routers mirror the domain model
- File structure mirrors the router tree
- Server calls use createCaller (not standalone functions)
## Transport
- HTTP: httpBatchLink
- WebSocket: wsLink + splitLink
- No manual fetch config
Why tRPC AI drift is predictable
REST is the default mental model for APIs. GraphQL is the default for "typed APIs." tRPC is newer (v10 stable in 2022, v11 in 2024) and has far less training data than either.
Without explicit instruction, AI writes tRPC as REST-with-types: procedures named like endpoints, manual type exports, fetch calls on the client instead of hooks.
The CLAUDE.md declares the tRPC model before any router is defined.
These 13 rules are part of a framework-specific CLAUDE.md series. The Cursor Rules Pack (50 production-tested rules across all stacks) is available at oliviacraftlat.gumroad.com/l/wyaeil — search "Olivia Craft Cursor Rules" on Gumroad.
Top comments (0)