TypeScript tRPC Agent Rules
Project Context
- Use tRPC v11 with TypeScript for end-to-end type safety without code generation.
- Use Zod for all input validation — tRPC integrates directly with Zod schemas.
- Use `@trpc/react-query` on the client for React hooks backed by TanStack Query.
- Configure `superjson` as the tRPC transformer for correct `Date`, `Map`, `Set`, and `BigInt` serialization.
Router Structure
- Define all routers in `src/server/routers/` — one file per domain (`users.ts`, `posts.ts`, `billing.ts`).
- Keep the root `appRouter` in `src/server/routers/_app.ts`, merging all sub-routers.
- Export the `AppRouter` type from `_app.ts` — import it on the client for `createTRPCReact<AppRouter>()`.
- Keep procedure handlers thin — delegate business logic to service functions in `src/server/services/`.
- Use `router()` and `mergeRouters()` to compose the final app router from feature sub-routers.
Procedures
- Use `publicProcedure` for unauthenticated endpoints; `protectedProcedure` for authenticated ones.
- Use `procedure.input(zodSchema).query(handler)` for reads and `.mutation(handler)` for writes.
- Return only the data the client needs — never return full database row objects if only a few fields are used.
- Use `.input()` with Zod `.transform()` to normalize data (trim strings, parse dates) before handlers run.
- Avoid side effects in `query` procedures. Mutations own all writes, deletions, and state changes.
Input Validation
- Validate all inputs with explicit Zod schemas. Never accept `z.any()` or `z.unknown()` in route inputs.
- Use `z.string().uuid()` for ID fields, `z.string().email()` for emails, `z.coerce.number()` for numeric params.
- Use `z.refine()` and `z.superRefine()` for cross-field validation with custom error messages.
- Co-locate input schemas with their router file, or place shared schemas in `src/server/schemas/`.
- Use `inferRouterInputs<AppRouter>` and `inferRouterOutputs<AppRouter>` to derive types for shared use.
Context & Middleware
- Build context in `createTRPCContext` from the request — include session, database client, and request ID.
- Create `protectedProcedure` by chaining `.use(auth middleware)` that validates the session and throws if missing.
- Use `.use()` for cross-cutting middleware: rate limiting, audit logging, performance tracing.
- Pass only serializable values through context — no class instances with methods.
- Use `middleware` to compose procedure-level auth: `adminProcedure`, `ownerProcedure`.
Error Handling
- Throw `TRPCError` with appropriate codes: `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `INTERNAL_SERVER_ERROR`.
- Include a user-friendly `message` and optional `cause` (the original error) for debugging in the `onError` handler.
- Register a global `onError` handler on the server to log all errors with structured context.
- Wrap database calls in try/catch and convert ORM-specific errors into `TRPCError` instances.
React Query Integration
- Use `trpc.procedure.useQuery()` and `trpc.procedure.useMutation()` hooks from `createTRPCReact`.
- Implement optimistic updates with `onMutate`, `onError` (rollback), and `onSettled` callbacks.
- Invalidate related queries after mutations: `utils.users.list.invalidate()`.
- Use `trpc.useUtils()` for programmatic cache access, prefetching, and batch invalidation.
- Use `enabled: !!userId` option for conditional queries — never call hooks conditionally.
- Configure `staleTime` and `gcTime` per query based on data freshness requirements.
Next.js App Router Integration
- Use `experimental-edge` or `nodejs` runtime for the tRPC route handler in `app/api/trpc/[trpc]/route.ts`.
- Use `createServerSideHelpers` for prefetching tRPC data in React Server Components.
- Use `HydrateClient` from tRPC to pass prefetched query data from RSC to the client React tree.
- Call `trpc.procedure.prefetch()` in Server Components to warm the React Query cache before client hydration.
Performance
- Use cursor-based pagination in query procedures — never `OFFSET/LIMIT` for large datasets.
- Batch independent queries using `Promise.all` in procedure handlers.
- Use `useQuery` with `placeholderData: keepPreviousData` for paginated tRPC queries.
- Configure `responseMeta` to set `Cache-Control` headers on public `query` procedures.
Testing
- Test procedures using `appRouter.createCaller(mockContext)` in Vitest unit tests.
- Mock the database client in test context — use `vitest-mock-extended` for Prisma clients.
- Test input validation by passing invalid data and asserting a `TRPCError` with code `BAD_REQUEST`.
- Test auth middleware by calling protected procedures with an unauthenticated mock context.