DEV Community

Ned C
Ned C

Posted on • Edited on

5 .cursorrules That Actually Changed Cursor's Output (And 2 That Were Useless)

Everyone's sharing their .cursorrules files. But does anyone actually test if they work?

I ran before/after comparisons through Cursor CLI. Same prompt, different rules. Here’s what I found.


The 5 That Actually Work

1. The useEffect Guard

The rule:

Before writing a useEffect, ask: can this be computed during render? If the value can be derived from props or state without side effects, compute it directly instead of using useEffect with setState.
Enter fullscreen mode Exit fullscreen mode

What happened:

Without the rule, I asked Cursor to create a component showing a list of numbers and their sum. It gave me:

const [sum, setSum] = useState(0);

useEffect(() => {
  const total = numbers.reduce((acc, n) => acc + n, 0);
  setSum(total);
}, [numbers]);
Enter fullscreen mode Exit fullscreen mode

Classic unnecessary useEffect. With the rule? Cursor computed it directly:

const sum = numbers.reduce((total, n) => total + n, 0);
Enter fullscreen mode Exit fullscreen mode

It even added a comment: "no useEffect needed."

Why it works: The rule is specific and verifiable. Cursor can check "is this derived from props/state?" before reaching for useEffect.


2. Branded Types for IDs

The rule:

Use branded types for ID fields to prevent mixing up different entity IDs. For example, use `type UserId = string & { readonly brand: unique symbol }` instead of plain `string` for user IDs.
Enter fullscreen mode Exit fullscreen mode

What happened:

Without the rule, getUser(userId) and getOrder(orderId) both used string:

export async function getUser(userId: string): Promise<User>
export async function getOrder(orderId: string): Promise<Order>
Enter fullscreen mode Exit fullscreen mode

Nothing stops you from calling getUser(orderId). With the rule:

type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };

export async function getUser(userId: UserId): Promise<User>
export async function getOrder(orderId: OrderId): Promise<Order>
Enter fullscreen mode Exit fullscreen mode

Now TypeScript catches the mixup at compile time.

Why it works: The rule includes the exact pattern to use. Cursor doesn’t have to figure out what "type-safe IDs" means.


3. Discriminated Unions + Exhaustive Checks

The rule:

Use discriminated unions for state that has mutually exclusive variants. Add exhaustive checking with a `never` type in switch default cases to catch unhandled variants at compile time.
Enter fullscreen mode Exit fullscreen mode

What happened:

Both versions created a proper discriminated union for async state:

type AsyncState<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };
Enter fullscreen mode Exit fullscreen mode

But only the version with the rule added the exhaustive check:

default: {
  const _exhaustiveCheck: never = state;
  return _exhaustiveCheck;
}
Enter fullscreen mode Exit fullscreen mode

Why it works: Cursor already knows discriminated unions. The rule’s value is the specific never pattern it wouldn’t add otherwise.


Want to try these rules? The first three come from the free Starter Pack. Drop it in your project and test them yourself.


4. Strict tsconfig Flags

The rule:

Ensure tsconfig.json has noUnusedLocals and noUnusedParameters set to true.
Enter fullscreen mode Exit fullscreen mode

What happened:

Without the rule, Cursor created a string utility module with no tsconfig at all. With the rule, it created:

{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Why it works: It’s a binary check. Either the flags are there or they’re not. Cursor can verify compliance.


5. Companion File Creation

The rule:

When creating a page.tsx file in the Next.js app directory, ALWAYS create a corresponding error.tsx file in the same directory.
Enter fullscreen mode Exit fullscreen mode

What happened:

Without the rule: just page.tsx.

With the rule: both page.tsx AND error.tsx, with a proper error boundary component.

Cursor even noted it was "satisfying the workspace rule."

Why it works: The rule describes a concrete action Cursor can take. Create file X, also create file Y. No interpretation needed.


The 2 That Did Nothing

Dud #1: Server Actions Rule

The rule:

For Next.js App Router mutations, prefer Server Actions over API routes.
Enter fullscreen mode Exit fullscreen mode

What happened: Both versions used Server Actions. The control (no rule) already created:

"use server";

export async function saveUser(formData) { ... }
Enter fullscreen mode Exit fullscreen mode

Why it failed: Cursor already defaults to Server Actions for Next.js App Router. The rule just told it to do what it was going to do anyway. I basically wrote a rule that says "keep breathing."


Dud #2: "Write Clean Code"

The rule:

Write clean, maintainable code.
Enter fullscreen mode Exit fullscreen mode

What happened: Identical output. Both versions produced the same well-structured code.

Why it failed: It’s not actionable. What does "clean" mean? Cursor already tries to write decent code. Telling it to "write clean code" is like telling a chef to "make it taste good."


The Pattern

Rules that work have three things in common:

  1. Specific — They describe exact patterns, not vibes
  2. Verifiable — Cursor can check if it followed the rule
  3. Additive — They ask for something Cursor wouldn’t do by default

Rules that fail are vague, unverifiable, or redundant.

Before you add a rule, ask yourself: "Could I verify this in a 2-second code review?" If not, Cursor probably can’t either.


Want Rules That Actually Work?

I package production-ready .cursorrules for specific stacks. The rules above come from these packs.

Cursor Rules Starter Pack — Framework-specific rules for React, Next.js, Node, and TypeScript. Free to download.

Browse the free rules collection on GitHub — 33 rules across languages, frameworks, and practices.

Or follow me here for more Cursor experiments.


Over to You

What rules have you tried in Cursor? Found any that made a real difference, or any that looked good on paper but did nothing? Drop them below — I'm always testing new ones.


Check your setup: npx cursor-doctor scan — finds broken rules, conflicts, and token waste. Free on npm.


📋 I made a free Cursor Safety Checklist — a pre-flight checklist for AI-assisted coding sessions, based on actual experiments.

Get it free →

Top comments (0)