trpc — Cursor rule: what it does, how to install
Enforces best practices for building type-safe tRPC APIs in TypeScript projects. Focuses on modular routers, Zod schemas, and context handling.
.cursor/rules/trpc.mdcWhat trpc does
Targeting **/*.{ts,tsx} files, this rule guides the IDE assistant to write clean tRPC backends. It steers code generation away from giant monolithic routers toward feature-specific modules combined in a central root file. Global state is avoided by setting up a centralized createTRPCContext for database clients, sessions, and request headers.
The guidelines require strict runtime validation with Zod schemas for all mutations and queries, explicitly barring z.any(). Access control is handled declaratively with reusable middleware that throws TRPCError. For error handling, the rule formats Zod issues into structured data and mandates superjson to preserve types like Date and Set.
Path alias imports for AppRouter types can degrade inference into any, so this rule mandates relative imports on the client side.
What it helps with
- split monolithic API files into modular feature routers
- validate procedure inputs at runtime with Zod schemas
- enforce declarative auth checks using custom tRPC middleware
- preserve rich JavaScript types between server and client via superjson
Example prompts for the agent
- Create a feature router for user management using protected procedures
- Set up admin authorization middleware with an unstable_pipe check
- Define createTRPCContext handling database and auth session options
How to install trpc
Manually: put the files in place
.cursor/rules/trpc.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/trpc.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/trpc.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/trpc.mdManually: put the files in place
.clinerules/trpc.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about trpc
How to install trpc in Cursor?
Add the rule instructions to .cursor/rules/trpc.mdc in your repository root.
What does the trpc rule do?
It provides guidelines for Cursor when editing TypeScript files, enforcing modular routers, centralized context, Zod validation, and proper error formatting.
Why does it disallow path aliases for AppRouter imports?
TypeScript type inference occasionally breaks when resolving path aliases across client-server boundaries, causing tRPC types to degrade into any.
Source text
trpc.mdc · CC0-1.0
---
description: Definitive guidelines for building robust, type-safe, and maintainable tRPC APIs, emphasizing modularity, Zod validation, and efficient error handling.
globs: **/*.{ts,tsx}
---
# tRPC Best Practices
These rules provide a definitive guide for building tRPC APIs within our team. Adhere to these principles for consistent, high-quality, and type-safe backend development.
## 1. Modularize Routers by Feature
Organize your API into distinct, feature-specific routers. This improves maintainability, readability, and allows for easier scaling.
❌ **BAD: Monolithic Router**
```typescript
// src/server/api/root.ts
export const appRouter = t.router({
getUser: publicProcedure.query(() => { /* ... */ }),
createUser: protectedProcedure.mutation(() => { /* ... */ }),
getPost: publicProcedure.query(() => { /* ... */ }),
createPost: protectedProcedure.mutation(() => { /* ... */ }),
// ... hundreds of procedures
});
```
✅ **GOOD: Modular Routers**
```typescript
// src/server/api/routers/user.ts
export const userRouter = t.router({
getUser: publicProcedure.query(() => { /* ... */ }),
createUser: protectedProcedure.mutation(() => { /* ... */ }),
});
// src/server/api/routers/post.ts
export const postRouter = t.router({
getPost: publicProcedure.query(() => { /* ... */ }),
createPost: protectedProcedure.mutation(() => { /* ... */ }),
});
// src/server/api/root.ts
export const appRouter = t.router({
user: userRouter,
post: postRouter,
});
```
## 2. Centralize Context Definition
Define your `createTRPCContext` function once to provide shared resources (e.g., database client, session, headers) to all procedures. This ensures consistency and avoids prop-drilling or global state.
```typescript
// src/server/api/context.ts
import { type inferAsyncReturnType } from '@trpc/server';
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
import { db } from '../db'; // Your database client
import { getServerAuthSession } from '../auth'; // Your auth session helper
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
const session = await getServerAuthSession({ req, res });
return {
db,
session,
headers: opts.req.headers,
};
};
export type Context = inferAsyncReturnType<typeof createTRPCContext>;
```
## 3. Enforce Input Validation with Zod
Always validate procedure inputs using Zod schemas. This provides runtime validation mirroring compile-time types, preventing malformed data from reaching business logic and improving API robustness.
❌ **BAD: No Input Validation**
```typescript
export const postRouter = t.router({
createPost: protectedProcedure
.input(z.any()) // Avoid z.any()
.mutation(async ({ ctx, input }) => {
// Manual validation or no validation
if (typeof input.title !== 'string' || input.title.length < 3) {
throw new Error('Invalid title');
}
return ctx.db.post.create({ data: input });
}),
});
```
✅ **GOOD: Zod for Input Validation**
```typescript
import { z } from 'zod';
export const postRouter = t.router({
createPost: protectedProcedure
.input(z.object({
title: z.string().min(3, "Title must be at least 3 characters"),
content: z.string().optional(),
}))
.mutation(async ({ ctx, input }) => {
// Input is guaranteed to be type-safe and validated
return ctx.db.post.create({ data: input });
}),
});
```
## 4. Implement Middleware for Authorization
Use tRPC middleware to enforce authentication and authorization rules declaratively. This keeps procedure logic clean and ensures consistent access control.
```typescript
// src/server/api/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import superjson from 'superjson';
import { ZodError } from 'zod';
import { type Context } from './context';
const t = initTRPC.context<Context>().create({
transformer: superjson,
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;
// Middleware to check for authenticated session
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
// Infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
});
});
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
// Example: Admin-only procedure
const enforceUserIsAdmin = enforceUserIsAuthed.unstable_pipe(({ ctx, next }) => {
if (ctx.session.user.role !== 'ADMIN') { // Assuming a 'role' on user
throw new TRPCError({ code: 'FORBIDDEN', message: 'Only admins can perform this action.' });
}
return next();
});
export const adminProcedure = t.procedure.use(enforceUserIsAdmin);
```
## 5. Standardize Error Formatting
Configure `errorFormatter` in `initTRPC.create` to provide consistent and helpful error responses, especially for Zod validation errors.
```typescript
// src/server/api/trpc.ts (excerpt)
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
// Flatten Zod errors for client-side consumption
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
```
## 6. Use Superjson for Data Serialization
Always use `superjson` as your data transformer. It correctly serializes and deserializes complex types (like `Date`, `Map`, `Set`) between client and server, maintaining type fidelity.
```typescript
// src/server/api/trpc.ts (excerpt)
import superjson from 'superjson';
const t = initTRPC.context<Context>().create({
transformer: superjson, // Essential for type-safe serialization
// ...
});
```
## 7. Prefer Relative Imports for tRPC Types
When importing `AppRouter` or other tRPC-related types into your client-side code, use relative paths instead of TypeScript path aliases. tRPC's type inference can sometimes struggle with alias resolution, leading to `any` types.
❌ **BAD: Path Alias for tRPC Types**
```typescript
// src/utils/trpc.ts
import { type AppRouter } from '@/server/api/root'; // May resolve to 'any'
```
✅ **GOOD: Relative Path for tRPC Types**
```typescript
// src/utils/trpc.ts
import { type AppRouter } from '../../server/api/root'; // Ensures correct type inference
```
## 8. Implement Robust Client-Side Invalidation
When using tRPC with `@tanstack/react-query`, ensure proper cache invalidation after mutations. This prevents stale data from being displayed and ensures UI consistency.
```typescript
// src/components/PostForm.tsx
import { api } from '@/utils/api'; // Your tRPC client instance
function PostForm() {
const utils = api.useUtils(); // Access TanStack Query client
const createPost = api.post.createPost.useMutation({
onSuccess: () => {
// Invalidate all queries under the 'post' router to refetch lists/details
utils.post.invalidate();
// For more granular control:
// utils.post.getPosts.invalidate();
},
});
// ... form submission logic
const handleSubmit = (data: { title: string; content?: string }) => {
createPost.mutate(data);
};
return (/* ... */);
}
```
## 9. Document Procedures with JSDoc
Add comprehensive JSDoc comments to each procedure. This improves developer experience by providing immediate context and usage instructions in the IDE.
```typescript
// src/server/api/routers/post.ts
export const postRouter = t.router({
/**
* Fetches all posts from the database.
* @returns A list of posts.
*/
getPosts: publicProcedure
.query(async ({ ctx }) => {
return ctx.db.post.findMany();
}),
/**
* Creates a new post.
* @param input - The post data (title, content).
* @returns The newly created post.
* @throws TRPCError if user is unauthorized.
*/
createPost: protectedProcedure
.input(z.object({
title: z.string().min(3),
content: z.string().optional(),
}))
.mutation(async ({ ctx, input }) => {
return ctx.db.post.create({ data: { ...input, authorId: ctx.session.user.id } });
}),
});
```
## 10. Enable TypeScript Strict Mode
Always develop with TypeScript's `strict` mode enabled in `tsconfig.json`. This catches a wide range of common programming errors at compile time, maximizing tRPC's type-safety benefits.
```json
// tsconfig.json
{
"compilerOptions": {
"strict": true, // Essential for robust type-safety
// ... other options
},
// ...
}
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc