Skip to main content
rule

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.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/trpc.mdc
Open on GitHub

What 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.mdc

Frontmatter: description, globs, alwaysApply

Step 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

Similar IDE rules

All IDE rules for Backend and APIs →

From the same set240

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor