$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

CLAUDE.md for tRPC Projects

tRPC eliminates the API layer — AI generates REST endpoints alongside tRPC. Rules for procedures, routers, Zod validation, React Query integration, and type-safe client.

7 min read·September 10, 2025

tRPC gives you end-to-end type safety — AI generates fetch() calls that bypass it

Procedures, Zod schemas, routers, React Query hooks, and typed middleware

Why tRPC Needs Rules That Prevent REST Patterns

tRPC's entire value proposition is end-to-end type safety: define a procedure on the server, call it from the client, and TypeScript validates the types across the boundary — no code generation, no schema files, no runtime validation overhead. AI assistants don't understand this model. They generate REST endpoints (app.get, app.post) alongside tRPC procedures, create manual fetch calls instead of using the typed client, and skip Zod validation (defeating the type-safe input contract).

The result is a project that uses tRPC's infrastructure (the router, the adapter) without its core benefit (type safety). If the AI generates a fetch("/api/users") call, it's bypassed the type-safe client entirely — the response is unknown, the input isn't validated, and you've lost the reason you chose tRPC.

These rules target tRPC v11+ with Next.js or standalone Node.js. They cover procedures, routers, middleware, Zod validation, and React Query integration.

Rule 1: Procedures for All API Operations

The rule: 'Define all API operations as tRPC procedures — never REST endpoints alongside tRPC. Use publicProcedure for unauthenticated operations, protectedProcedure (with auth middleware) for authenticated operations. Queries for reads: .query(async ({ input, ctx }) => { ... }). Mutations for writes: .mutation(async ({ input, ctx }) => { ... }). Subscriptions for realtime: .subscription(({ input, ctx }) => { ... }).'

For router composition: 'Organize procedures into routers by domain: const userRouter = router({ getById: ..., create: ..., update: ... }). Compose routers: const appRouter = router({ user: userRouter, post: postRouter, auth: authRouter }). Export the type: export type AppRouter = typeof appRouter. This type is what the client imports for end-to-end type safety.'

AI generates app.get("/api/users/:id") next to tRPC routers — creating two parallel APIs. One rule ('all API operations are tRPC procedures') eliminates the REST shadow API.

  • publicProcedure for unauth — protectedProcedure for auth
  • .query() for reads — .mutation() for writes — .subscription() for realtime
  • Router per domain: userRouter, postRouter, authRouter
  • Compose: appRouter = router({ user: userRouter, post: postRouter })
  • Export type AppRouter — the client imports this for type safety
⚠️ No REST Shadow API

AI generates app.get('/api/users') next to tRPC routers — two parallel APIs. fetch('/api/users') bypasses the type-safe client entirely. One rule: 'all API operations are tRPC procedures' eliminates the shadow REST API.

Rule 2: Zod Input Schemas on Every Procedure

The rule: 'Define Zod input schemas on every procedure that accepts input: .input(z.object({ id: z.string().uuid() })). The schema validates at runtime AND provides TypeScript types at compile time — one definition, two guarantees. Never use untyped input — if a procedure has no .input(), it accepts no input (not unvalidated input). Use z.object for all inputs, even simple ones.'

For output types: 'Use .output(z.object({ ... })) when you want to: document the response shape in the type, validate the server's output (catch bugs where the server returns wrong data), and strip extra fields from the response. Output schemas are optional but recommended for public APIs and shared procedures.'

AI generates procedures without .input() and manually parses data inside the handler — losing both runtime validation and compile-time types. Zod schemas on every procedure are tRPC's contract — what the client sends, what the server validates, and what TypeScript checks.

💡 One Schema, Two Guarantees

.input(z.object({ id: z.string().uuid() })) validates at runtime AND provides TypeScript types at compile time. One Zod schema definition gives you both. Procedures without .input() accept no input — not unvalidated input.

Rule 3: Middleware and Context

The rule: 'Use tRPC middleware for cross-cutting concerns: auth checking, logging, rate limiting. Define middleware: const isAuthed = t.middleware(({ ctx, next }) => { if (!ctx.session) throw new TRPCError({ code: "UNAUTHORIZED" }); return next({ ctx: { ...ctx, user: ctx.session.user } }); }). Create procedure builders: const protectedProcedure = t.procedure.use(isAuthed). All authenticated procedures use protectedProcedure — consistent auth checking.'

For context: 'Create context in the tRPC initialization: createContext = async ({ req, res }) => { const session = await getSession(req); return { session, db, req, res }; }. Context is available in every procedure via ctx. Use context for: database connection, authenticated user, request metadata. Never access req/res directly in procedures — go through context.'

For error handling: 'Throw TRPCError with appropriate codes: UNAUTHORIZED, FORBIDDEN, NOT_FOUND, BAD_REQUEST, INTERNAL_SERVER_ERROR. TRPCError maps to HTTP status codes in the adapter. Never throw generic Error — TRPCError provides typed error codes that the client can handle.'

Rule 4: React Query Integration

The rule: 'Use tRPC's React Query hooks for all data fetching: const { data, isLoading, error } = trpc.user.getById.useQuery({ id }). For mutations: const mutation = trpc.user.create.useMutation(). These hooks are fully typed — the input type matches the procedure's Zod schema, the output type matches the procedure's return type. Never use raw useQuery or useMutation with fetch — it bypasses type safety.'

For invalidation: 'Invalidate queries after mutations: useMutation({ onSuccess: () => utils.user.getById.invalidate({ id }) }). Use utils (from trpc.useUtils()) for programmatic cache management. Invalidation is typed — you can only invalidate procedures that exist on the router.'

For SSR/SSG: 'In Next.js, use tRPC's SSR helpers: const helpers = createServerSideHelpers({ router: appRouter, ctx }); await helpers.user.getById.prefetch({ id }); return { props: { trpcState: helpers.dehydrate() } }. Data is prefetched on the server and hydrated on the client — no loading spinner on initial render.'

  • trpc.user.getById.useQuery({ id }) — fully typed, auto-cached
  • trpc.user.create.useMutation() — typed input and output
  • utils.user.getById.invalidate() — typed cache invalidation
  • Never raw useQuery + fetch — bypasses type safety entirely
  • SSR helpers: prefetch on server, dehydrate, hydrate on client — no spinner
ℹ️ Typed Hooks, Not Fetch

trpc.user.getById.useQuery({ id }) is fully typed: the input matches the Zod schema, the output matches the return type. useQuery + fetch('/api/users') returns unknown. The typed hook IS the reason you chose tRPC.

Rule 5: tRPC-Specific Patterns

The rule: 'Use tRPC's batching: multiple useQuery calls in one component are batched into a single HTTP request automatically. Use infinite queries: trpc.post.list.useInfiniteQuery for paginated data. Use optimistic updates: useMutation({ onMutate: async (newData) => { utils.post.list.setData(undefined, (old) => [...old, newData]) } }).'

For testing: 'Test procedures directly without HTTP: const caller = appRouter.createCaller(ctx); const result = await caller.user.getById({ id }). This tests the procedure logic, middleware, and Zod validation without starting a server. Use this for unit tests — integration tests should go through the HTTP adapter.'

For the standalone adapter: 'For non-Next.js projects, use @trpc/server/adapters/standalone or @trpc/server/adapters/express. The adapter translates HTTP requests to procedure calls. The router and procedures are the same regardless of adapter — swap adapters without changing business logic.'

Complete tRPC Rules Template

Consolidated rules for tRPC projects.

  • All API operations as tRPC procedures — never REST endpoints alongside tRPC
  • Zod .input() on every procedure — runtime validation + compile-time types
  • Router per domain — compose into appRouter — export type AppRouter
  • Middleware for auth: protectedProcedure = procedure.use(isAuthed)
  • TRPCError with typed codes — never generic Error throws
  • React Query hooks: useQuery, useMutation — never raw fetch
  • utils for typed cache invalidation — SSR helpers for prefetching
  • createCaller for testing without HTTP — batching and infinite queries built in