Skip to content

Web Development · Developer Tools

Cursor Rules for Teams: Codifying Your Standards for AI Coding

Cursor's rules system lets teams encode their architecture decisions, naming conventions, and coding standards into the AI's context. Here's how to set it up so every engineer gets consistent suggestions.

Anurag Verma

Anurag Verma

7 min read

Cursor Rules for Teams: Codifying Your Standards for AI Coding

Sponsored

Share

AI coding assistants are only as good as the context they have. By default, Cursor knows the patterns in your open file and whatever it can infer from the rest of the codebase. It doesn’t know that your team uses Zod for validation instead of Yup, that service functions should never be called directly from route handlers, or that you have a wrapper around fetch that handles error logging.

Cursor rules fix that. They inject persistent context into every AI interaction in your project: how your specific codebase works, what patterns you follow, and what the AI should and shouldn’t suggest.

How Cursor Rules Work

Cursor moved from a single .cursorrules file to a .cursor/rules/ directory in late 2024. The directory format is more flexible: you can have multiple rule files, apply them conditionally based on file path, and toggle some off when not needed.

Rule files use the .mdc extension (Markdown with Cursor metadata). The frontmatter controls when and how the rule applies:

---
description: TypeScript and React conventions
globs: ["**/*.ts", "**/*.tsx"]
alwaysApply: false
---

## Code Style

Use named exports, not default exports.
Prefer `interface` over `type` for object shapes.
Use `const` everywhere; never `let` for primitives.

## Component Structure

React components follow this structure:
1. Type definitions
2. Component function
3. Local hooks/helpers
4. Export

Components must not import from other feature directories.
Cross-feature dependencies go through the shared/ layer.

The globs field limits when Cursor loads this rule file. A rule file with globs: ["**/*.ts", "**/*.tsx"] only applies when you’re working in TypeScript/TSX files. A rule file with alwaysApply: true loads for every file in the project.

Directory Layout

A well-organized .cursor/rules/ directory for a Next.js project:

.cursor/
  rules/
    architecture.mdc       # alwaysApply: true (project structure, invariants)
    typescript.mdc         # globs: **/*.ts, **/*.tsx
    api-routes.mdc         # globs: app/api/**/*.ts
    database.mdc           # globs: lib/db/**/*.ts, **/*.sql
    testing.mdc            # globs: **/*.test.ts, **/*.spec.ts
    react-components.mdc   # globs: **/*.tsx

The architecture.mdc file sets the foundation that every other rule builds on:

---
description: Project architecture overview
alwaysApply: true
---

## Project Overview

E-commerce platform. Next.js 15 App Router. PostgreSQL via Drizzle ORM. 
Stripe for payments. Resend for transactional email.

## Layer Rules

- UI: app/ directory, .tsx files only, React Server Components by default
- API: app/api/ for REST endpoints, server actions for form submissions
- Business logic: lib/ directory, pure TypeScript functions
- Database: lib/db/, Drizzle schema and queries only, never raw SQL in routes
- External services: lib/integrations/, each service gets its own file

Business logic must not import from app/ (no circular dependencies).
Database queries must not be called from UI components; always go through lib/.

## Error Handling

All async functions return { data, error } objects. Never throw in business logic.
Server actions use next/navigation redirect() for success paths.
API routes return NextResponse.json() with consistent { success, data, error } shape.

## No-Go Zones

- Never use `any` type (use `unknown` and narrow)
- Never use `console.log` in production paths (use the logger at lib/logger.ts)
- Never call Stripe directly from components (always via lib/integrations/stripe.ts)

TypeScript-Specific Rules

---
description: TypeScript patterns and conventions
globs: ["**/*.ts", "**/*.tsx"]
alwaysApply: false
---

## Types and Interfaces

Use `interface` for object types that describe shapes.
Use `type` for unions, intersections, and utility types.

Prefer explicit return types on exported functions:
```typescript
// Good
export function getUser(id: string): Promise<User | null> { ... }

// Avoid
export function getUser(id: string) { ... }

Zod is used for runtime validation. Define schemas in the same file as the business logic that uses them. Export inferred types:

const CreateOrderSchema = z.object({
  items: z.array(z.object({ productId: z.string(), quantity: z.number().int().positive() })),
  shippingAddressId: z.string(),
});
type CreateOrderInput = z.infer<typeof CreateOrderSchema>;

Async/Error Patterns

This project uses the Result pattern for error handling:

type Result<T, E = Error> = { ok: true; data: T } | { ok: false; error: E };

The helper is at lib/result.ts. Always use it for functions that can fail. Never use try/catch in service functions; let errors propagate to the boundary.


## Component Rules for React

```markdown
---
description: React component conventions
globs: ["**/*.tsx"]
alwaysApply: false
---

## Server vs Client Components

Default to Server Components. Add `"use client"` only when the component:
- Uses hooks (useState, useEffect, useRef, etc.)
- Attaches event listeners
- Uses browser-only APIs

Mark the boundary as low in the tree as possible. Don't make a parent 
component a Client Component just because one child needs it.

## Props and Types

All component props use TypeScript interface:
```typescript
interface ButtonProps {
  variant: "primary" | "secondary" | "ghost";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  children: React.ReactNode;
  onClick?: () => void;
}

Don’t use React.FC or React.FunctionComponent; just define the function directly.

Data Fetching

Components fetch their own data in Server Components:

export default async function OrderList({ userId }: { userId: string }) {
  const orders = await getOrdersByUser(userId);
  return <ul>{orders.map(order => <OrderItem key={order.id} order={order} />)}</ul>;
}

No prop-drilling of data from page to child components when the child can fetch its own data server-side.


## Committing Rules to Version Control

Cursor rules go in the repository. Everyone on the team gets the same rules when they clone the project.

```bash
git add .cursor/
git commit -m "chore: add cursor rules for project conventions"

Add a note in your README or onboarding docs explaining what the rules cover. New engineers should know these files exist and read them before diving into code.

What Makes a Good Rule

Rules that help:

  • Explain patterns that aren’t obvious from reading the code (“we use X instead of Y because Z”)
  • Describe structural invariants (“database queries only in lib/db/, never in routes”)
  • Define naming conventions (“service functions use verb-noun: getUser, createOrder, deleteSession”)
  • Specify which libraries handle which concerns (“use Resend for email, not Nodemailer”)
  • Describe error handling patterns that the codebase uses throughout

Rules that don’t help:

  • Generic advice the AI already knows (“write clean code”, “use meaningful variable names”)
  • Documentation about how a library works (the AI already knows that)
  • Rules so long that the relevant part gets diluted by noise
  • Contradictory rules in different files

Keep rule files short. A rule file that’s 50 lines with specific, project-specific guidance is more useful than a 300-line file with a mix of project-specific and generic advice.

Comparing with GitHub Copilot Instructions

GitHub Copilot has a similar feature: .github/copilot-instructions.md. It’s a single file that applies globally to all Copilot interactions in the repository.

Cursor’s rules system is more granular (different rules for different file types) and the MDC frontmatter gives you more control over when rules load. Copilot’s approach is simpler to set up but less precise. If your team uses Copilot, the .github/copilot-instructions.md file is worth maintaining with your project’s key conventions. The principle is the same.

The Compounding Return

The value of Cursor rules compounds as the project grows. Early on, the AI can infer a lot from a small, consistent codebase. Later, with hundreds of files, patterns accumulate and drift. New features might introduce a new pattern for error handling. A different engineer might reach for a different library. Without rules, the AI starts suggesting based on whatever it sees in the local context, which may be inconsistent.

Rules make the AI’s suggestions match the codebase’s actual conventions rather than the AI’s training defaults. For teams where multiple engineers are using AI assistance simultaneously, that consistency is worth the hour it takes to write the initial rule files.

The other return: rules are good documentation. A new engineer reading .cursor/rules/architecture.mdc gets a faster mental model of the project than reading the codebase cold. The rules describe the “why” behind patterns in a way that code doesn’t.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored