Overview

A Cursor project rule is a persistent system prompt that Cursor injects into AI conversations in the project. A well-written rules file produces code that fits the codebase from the first suggestion, without per-session re-prompting. The current format is one or more .mdc files under .cursor/rules/, not the legacy single .cursorrules file at the repo root: Cursor’s Agent mode, the default mode since 2026, silently ignores .cursorrules, so a team relying on it stops getting rule-following behavior without any error. The principles that make a system prompt effective are the same ones in system-prompts; this guide applies them to a TypeScript project.

Prerequisites

  • Cursor IDE installed, recent enough that Agent mode is the default (any 2026 release).
  • A TypeScript project with a tsconfig.json. The rules reference TypeScript-specific conventions.
  • Familiarity with the project’s style, testing framework, and folder structure. Rules you cannot verify are rules that drift.

Steps

1. Create the rules directory

mkdir -p .cursor/rules
touch .cursor/rules/project-conventions.mdc

Cursor reads every .mdc file under .cursor/rules/ automatically. Split rules into multiple files by concern (typescript.mdc, testing.mdc) once the project grows past one topic; a single file is fine to start. Commit the directory with the code; it is project documentation.

2. Write the frontmatter

Each .mdc file opens with YAML frontmatter that controls when Cursor loads it: description (used for intelligent matching when alwaysApply is false), globs (file patterns that force-activate the rule), and alwaysApply (loads on every conversation regardless of context).

---
description: Project conventions for the Next.js storefront
globs: ["**/*.ts", "**/*.tsx"]
alwaysApply: false
---

Set alwaysApply: true only for rules that must load every time, such as voice or architecture rules; overusing it burns context budget on every conversation.

3. Open with a mission statement

Start the markdown body with one or two sentences that name the project, its purpose, and the main stack.

This is a Next.js 15 + TypeScript e-commerce storefront.
The codebase uses the App Router, Tailwind CSS, Prisma ORM, and Postgres.

Keep it specific. “This is a web project” adds no signal. See prompt-design for the general rule on specificity.

4. Set voice and style rules

Voice rules:
- Lead with the implementation, then the explanation.
- Prefer short, named functions over long anonymous callbacks.
- No commented-out code in commits.
- No TODO comments without a ticket reference.

Match the voice rules to what the team actually enforces in code review. If the team does not care about TODO comments, omit that rule.

5. Define TypeScript conventions

TypeScript rules:
- Strict mode is on. Never use `any`; use `unknown` and narrow it.
- Prefer `interface` for public API shapes; use `type` for unions and mapped types.
- Export types from `types.ts` in each feature folder.
- Use `zod` for runtime validation at API boundaries.
- Avoid `as` casts; if a cast is unavoidable, add a comment explaining why.

These map directly to the patterns in typescript-strict-mode. See typescript for the full type-level conventions.

6. Set folder and file conventions

File conventions:
- React components live in `src/components/<ComponentName>/index.tsx`.
- Server actions live in `src/app/actions/<feature>.ts`.
- Database queries live in `src/lib/db/<entity>.ts`.
- One component per file. Do not export multiple components from one file.
- Barrel files (`index.ts`) are allowed only in `src/components`.

Concrete paths prevent the AI from placing files in unexpected locations.

7. Define test expectations

Testing rules:
- Tests live next to the file they test: `foo.ts` and `foo.test.ts`.
- Use Vitest, not Jest.
- Test behavior, not implementation. Do not test private methods.
- Every function exported from `src/lib` must have at least one test.
- Use `describe` blocks to group related tests; keep `it` descriptions in plain English.

See testing for the broader testing philosophy.

8. Add a complete example

Examples anchor the rules in concrete output. Include at minimum one before/after pair.

Example: API route with validation

// Good
import { z } from "zod"
const schema = z.object({ email: z.string().email() })
export async function POST(req: Request) {
  const body = schema.safeParse(await req.json())
  if (!body.success) return Response.json({ error: body.error.flatten() }, { status: 400 })
  // ...
}

// Avoid: no runtime validation
export async function POST(req: Request) {
  const { email } = await req.json()
  // email is any; no validation
}

9. Scope and length

Keep each .mdc file under 200 lines. Rules past that point compete for attention and dilute signal. Prioritize the conventions that are:

  1. Hard to enforce via a linter.
  2. Frequently violated by AI-generated code.
  3. Specific to this project (not just TypeScript best practices generally).

Verify it worked

  1. Open a new Cursor chat in Agent mode (not a file-level inline suggestion) and ask: “How should I structure a new API route in this project?”
  2. Confirm the answer references the folder conventions and Zod validation from the rules.
  3. Ask it to write a test for a utility function. Confirm it uses Vitest and places the test file correctly.
  4. If the AI ignores the rules, confirm the files are .mdc files under .cursor/rules/, not a legacy .cursorrules file; Agent mode does not read .cursorrules at all.

Common errors

  • The AI ignores the rules and no error appears. The project still relies on a root .cursorrules file; Cursor’s Agent mode, the default since 2026, silently ignores it. This is the failure most likely to bite a team that set rules up a year or two ago and never migrated. Move the content into .cursor/rules/*.mdc.
  • A rule never activates. alwaysApply is false and the globs pattern does not match the file being edited, and the description is too vague for intelligent matching to select it. Broaden the globs or sharpen the description.
  • Rules conflict with each other. “Use functional components” and “class components are fine for stateful logic” cannot coexist. Resolve the conflict before committing.
  • Rules are too abstract. “Write clean code” is not a rule; “functions must be under 30 lines” is. Replace abstractions with measurable criteria.
  • Rules become stale as the stack evolves. Treat the rules directory like a CLAUDE.md: review it when the framework, test runner, or style guide changes. See claude-code for the anchor-file maintenance pattern.