guide12 min read8d ago

Best Cursor Rules for Next.js & React (2026)

15 battle-tested .cursorrules configurations for Next.js and React projects. Copy-paste ready. From App Router conventions to testing patterns, these rules make Cursor actually useful.

Best Cursor Rules for Next.js & React (2026)
cursorcursor rulescursorrulesnext.jsreactAI codingdeveloper tools2026

Best Cursor Rules for Next.js & React (2026)

By David Henderson | March 26, 2026 | 14 min read


TL;DR: Cursor is powerful out of the box, but it generates generic code unless you tell it how your project works. A .cursorrules file at the root of your project fixes that. Below are 15 copy-paste configurations I use daily on Next.js and React projects — from App Router conventions to component patterns to testing setups. Grab the ones that match your stack.

Table of Contents

  1. Why Cursor Rules Matter
  2. How .cursorrules Files Work
  3. The 15 Rules
  4. How to Combine Rules for Your Project
  5. Frequently Asked Questions

Why Cursor Rules Matter {#why-cursor-rules-matter}

I have been using Cursor on Next.js projects for over a year. The single biggest upgrade was not a model change or a new feature — it was writing a proper .cursorrules file.

Without rules, Cursor will:

  • Generate Pages Router code when you are using App Router
  • Use getServerSideProps instead of server components
  • Create class components instead of functional ones
  • Import from next/router instead of next/navigation
  • Use CSS modules when your project uses Tailwind
  • Ignore your project's naming conventions entirely

A .cursorrules file is a plain text file in your project root that tells Cursor how your codebase works. It is the difference between Cursor generating code you have to rewrite and Cursor generating code you can ship.

Every rule below is something I have tested on production Next.js and React projects. They are not theoretical. They solve real problems.

If you want to explore hundreds more community-contributed Cursor rules, check out the Skiln Cursor directory — we maintain the largest searchable collection.


How .cursorrules Files Work {#how-cursorrules-work}

Create a file called .cursorrules in your project root (same level as package.json). Cursor reads this file automatically every time it generates code for your project.

The file is plain text. No special syntax required. Write instructions in natural language, and Cursor follows them. You can also include code examples — Cursor treats them as patterns to follow.

A few things to know:

  • One file per project. Cursor reads from the project root.
  • Order matters loosely. Put the most important rules first.
  • Be specific. "Use Tailwind" is okay. "Use Tailwind CSS with the cn() utility from @/lib/utils for conditional classes" is better.
  • Include examples. Show Cursor what good output looks like.
  • Keep it under 2,000 words. Cursor has context limits. Shorter rules with examples beat long rules without them.

Now let's get to the rules.


The 15 Rules {#the-15-rules}

Rule 1: Next.js App Router Foundation

This is the baseline rule for any Next.js 14+ project using the App Router. Start here.

# Next.js App Router Project

This project uses Next.js 15 with the App Router (not Pages Router).

## Core Conventions
- All routes live in `app/` directory, not `pages/`
- Use `next/navigation` for routing (useRouter, usePathname, useSearchParams) — NEVER `next/router`
- Default to Server Components. Only add "use client" when the component needs browser APIs, event handlers, or React hooks (useState, useEffect, etc.)
- Use `loading.tsx` for Suspense boundaries, `error.tsx` for error boundaries, `not-found.tsx` for 404s
- Metadata is exported from `layout.tsx` or `page.tsx` using the `metadata` export or `generateMetadata()` function — NEVER use `<Head>` from `next/head`
- Use `next/image` for all images with proper width/height or fill prop
- API routes go in `app/api/` as `route.ts` files using NextRequest/NextResponse

## File Naming
- Components: PascalCase (e.g., `UserProfile.tsx`)
- Utilities: camelCase (e.g., `formatDate.ts`)
- Route files: lowercase (e.g., `page.tsx`, `layout.tsx`)

## Example Server Component
export default async function DashboardPage() {
  const data = await fetchDashboardData();
  return <Dashboard data={data} />;
}

## Example Client Component
"use client";
import { useState } from "react";
export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Rule 2: Tailwind CSS + cn() Utility

For projects using Tailwind with the popular cn() pattern from shadcn/ui.

# Tailwind CSS

This project uses Tailwind CSS for all styling. Do NOT use:
- CSS Modules
- Styled Components
- Inline style objects (except for truly dynamic values like calculated positions)
- Plain CSS files

Use the `cn()` utility from `@/lib/utils` for conditional class merging:

import { cn } from "@/lib/utils";

<div className={cn(
  "flex items-center gap-2 rounded-lg p-4",
  isActive && "bg-primary text-primary-foreground",
  isDisabled && "opacity-50 pointer-events-none"
)} />

Prefer Tailwind's built-in responsive prefixes (sm:, md:, lg:) over custom media queries.
Use Tailwind's color system — never hardcode hex values unless matching a specific brand color.

Rule 3: TypeScript Strict Mode

Prevents Cursor from generating sloppy TypeScript.

# TypeScript Standards

This project uses TypeScript in strict mode. Follow these rules:
- NEVER use `any` type. Use `unknown` if the type is truly unknown, then narrow it.
- NEVER use `@ts-ignore` or `@ts-expect-error` — fix the type instead.
- Always define return types for exported functions.
- Use interface for object shapes, type for unions/intersections.
- Prefer `as const` over enum for string unions.
- Use zod for runtime validation at API boundaries.

## Example
interface UserProps {
  id: string;
  name: string;
  role: "admin" | "user" | "viewer";
}

export function getUserDisplayName(user: UserProps): string {
  return user.role === "admin" ? `${user.name} (Admin)` : user.name;
}

Rule 4: Server Actions Pattern

For projects using Next.js Server Actions instead of API routes.

# Server Actions

Use Next.js Server Actions for mutations. Do NOT create API routes for form submissions or data mutations.

## Pattern
- Server actions live in `app/_actions/` directory
- Each action file exports named functions with "use server" directive
- Always validate input with zod before processing
- Return typed results, never throw errors to the client

## Example
// app/_actions/user.ts
"use server";
import { z } from "zod";

const updateProfileSchema = z.object({
  name: z.string().min(1).max(100),
  bio: z.string().max(500).optional(),
});

export async function updateProfile(formData: FormData) {
  const parsed = updateProfileSchema.safeParse({
    name: formData.get("name"),
    bio: formData.get("bio"),
  });

  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors };
  }

  // ... update database
  return { success: true };
}

Rule 5: React Component Patterns

Establishes consistent component structure.

# React Component Conventions

## Structure (in this order)
1. "use client" directive (only if needed)
2. Imports
3. Type/interface definitions
4. Component function
5. Helper functions (below the component, not above)

## Rules
- Use named exports for components, not default exports (except page.tsx, layout.tsx)
- Props interface is always named {ComponentName}Props
- Destructure props in the function signature
- Use early returns for conditional rendering
- Keep components under 150 lines — extract sub-components if longer

## Example
"use client";

import { useState } from "react";
import { cn } from "@/lib/utils";

interface FeatureCardProps {
  title: string;
  description: string;
  isHighlighted?: boolean;
}

export function FeatureCard({ title, description, isHighlighted = false }: FeatureCardProps) {
  const [isExpanded, setIsExpanded] = useState(false);

  if (!title) return null;

  return (
    <div className={cn("rounded-lg border p-6", isHighlighted && "border-primary bg-primary/5")}>
      <h3 className="text-lg font-semibold">{title}</h3>
      <p className="mt-2 text-muted-foreground">{description}</p>
    </div>
  );
}

Rule 6: Data Fetching Patterns

Controls how Cursor handles data fetching in the App Router.

# Data Fetching

## Server Components (preferred)
Fetch data directly in server components using async/await. No useEffect. No loading states needed — use loading.tsx for that.

## Client Components
Use React Query (TanStack Query) for client-side data fetching. NEVER use useEffect + fetch + useState for data loading.

## Example (Server Component)
// app/dashboard/page.tsx
import { db } from "@/lib/db";

export default async function DashboardPage() {
  const stats = await db.query.stats.findMany();
  return <StatsGrid stats={stats} />;
}

## Example (Client Component with React Query)
"use client";
import { useQuery } from "@tanstack/react-query";

export function LiveStats() {
  const { data, isLoading } = useQuery({
    queryKey: ["live-stats"],
    queryFn: () => fetch("/api/stats/live").then(r => r.json()),
    refetchInterval: 5000,
  });

  if (isLoading) return <Skeleton />;
  return <StatsDisplay data={data} />;
}

Rule 7: shadcn/ui Components

For projects using the shadcn/ui component library.

# UI Components

This project uses shadcn/ui components from `@/components/ui/`.

## Rules
- ALWAYS check if a shadcn/ui component exists before creating a custom one
- Import from `@/components/ui/{component}` — never recreate primitives
- Available components: Button, Card, Dialog, Dropdown, Input, Label, Select, Separator, Sheet, Skeleton, Table, Tabs, Textarea, Toast, Tooltip
- Use the `variant` prop system — don't override with custom classes when a variant exists
- For forms, use react-hook-form + zod + shadcn Form components

## Example
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

<Card>
  <CardHeader>
    <CardTitle>Settings</CardTitle>
  </CardHeader>
  <CardContent>
    <Button variant="outline" size="sm">Save Changes</Button>
  </CardContent>
</Card>

Rule 8: Authentication with NextAuth/Auth.js

For projects using Auth.js (NextAuth v5).

# Authentication

This project uses Auth.js (NextAuth v5) for authentication.

## Setup
- Config lives in `auth.ts` at project root
- Middleware in `middleware.ts` protects routes
- Session is accessed via `auth()` in server components or `useSession()` in client components

## Rules
- ALWAYS check auth in server components: const session = await auth();
- Redirect unauthenticated users: if (!session) redirect("/login");
- NEVER expose user IDs to the client — use session.user.id only server-side
- Role checks happen server-side, never client-side only

## Example
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function AdminPage() {
  const session = await auth();
  if (!session) redirect("/login");
  if (session.user.role !== "admin") redirect("/unauthorized");

  return <AdminDashboard user={session.user} />;
}

Rule 9: Database with Drizzle ORM

For projects using Drizzle ORM.

# Database

This project uses Drizzle ORM with PostgreSQL.

## Structure
- Schema definitions in `drizzle/schema.ts`
- Migrations in `drizzle/migrations/`
- DB client in `lib/db.ts`

## Rules
- Use Drizzle's query API for reads (db.query.tableName.findMany)
- Use Drizzle's insert/update/delete for writes
- NEVER write raw SQL unless Drizzle cannot express the query
- Always use transactions for multi-table writes
- Define relations in the schema file

## Example
import { db } from "@/lib/db";
import { users, posts } from "@/drizzle/schema";
import { eq } from "drizzle-orm";

// Read
const user = await db.query.users.findFirst({
  where: eq(users.id, userId),
  with: { posts: true },
});

// Write
await db.insert(posts).values({
  title: "New Post",
  authorId: userId,
  content: "...",
});

Rule 10: Error Handling Pattern

Standardizes error handling across the project.

# Error Handling

## Server-Side
- Use try/catch at the boundary (Server Actions, API routes, data fetching)
- Log errors with structured logging (console.error with context, or a logger like pino)
- Return user-friendly error messages — never expose stack traces or internal details

## Client-Side
- Use error.tsx boundaries for route-level errors
- Use React Error Boundaries for component-level errors
- Show toast notifications for recoverable errors (failed form submissions)
- Show full error pages for unrecoverable errors (404, 500)

## Result Pattern (preferred for Server Actions)
type ActionResult<T> =
  | { success: true; data: T }
  | { success: false; error: string };

export async function createProject(formData: FormData): Promise<ActionResult<Project>> {
  try {
    const project = await db.insert(projects).values({...}).returning();
    return { success: true, data: project[0] };
  } catch (e) {
    console.error("Failed to create project:", e);
    return { success: false, error: "Failed to create project. Please try again." };
  }
}

Rule 11: Testing Setup

For projects with Vitest and React Testing Library.

# Testing

This project uses Vitest + React Testing Library for tests.

## Structure
- Tests live next to the code they test: `Component.tsx` → `Component.test.tsx`
- Test utilities in `tests/utils.tsx`
- Mock data in `tests/fixtures/`

## Rules
- Test behavior, not implementation details
- Use `screen.getByRole()` and `screen.getByText()` — avoid `getByTestId` except as last resort
- Mock external services (API calls, database) — never hit real services in tests
- Use `userEvent` over `fireEvent` for user interactions

## Example
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { FeatureCard } from "./FeatureCard";

test("expands description when clicked", async () => {
  render(<FeatureCard title="Fast" description="Ships in seconds" />);
  const card = screen.getByRole("button");
  await userEvent.click(card);
  expect(screen.getByText("Ships in seconds")).toBeVisible();
});

Rule 12: API Route Patterns

For Next.js API routes following consistent patterns.

# API Routes

API routes live in `app/api/` and use Next.js Route Handlers.

## Rules
- Always validate request body with zod
- Always return proper HTTP status codes
- Use NextRequest and NextResponse
- Add rate limiting headers for public endpoints
- Authenticate before processing

## Example
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/auth";

const createSchema = z.object({
  name: z.string().min(1).max(100),
  description: z.string().max(500).optional(),
});

export async function POST(request: NextRequest) {
  const session = await auth();
  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const body = await request.json();
  const parsed = createSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
  }

  // ... process request
  return NextResponse.json({ data: result }, { status: 201 });
}

Rule 13: Performance Rules

Prevents Cursor from generating slow patterns.

# Performance

## Rules
- Use dynamic imports for heavy components: const Chart = dynamic(() => import("./Chart"), { ssr: false });
- Add loading="lazy" to images below the fold
- Use React.memo() only when profiling shows re-render problems — do not premature optimize
- Prefer CSS animations over JS animations
- Use next/font for font loading — never load fonts from external CDN in <head>
- Use `unstable_cache` or `revalidateTag` for expensive database queries
- NEVER use `force-dynamic` on routes unless absolutely required — it kills static optimization

## Image Optimization
Always use next/image with explicit width and height:
<Image src="/hero.webp" alt="Description" width={1200} height={630} priority />

Use `priority` only for above-the-fold images (hero images, LCP elements).

Rule 14: Environment Variables

Controls how Cursor handles env vars.

# Environment Variables

## Rules
- Server-only env vars: `process.env.DATABASE_URL` (no prefix)
- Client-exposed env vars: `process.env.NEXT_PUBLIC_APP_URL` (NEXT_PUBLIC_ prefix)
- NEVER hardcode secrets, API keys, or credentials in code
- NEVER log environment variables
- Access env vars through a typed config file:

// lib/env.ts
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  NEXTAUTH_SECRET: z.string().min(32),
  NEXT_PUBLIC_APP_URL: z.string().url(),
});

export const env = envSchema.parse(process.env);

// Usage: import { env } from "@/lib/env"; then env.DATABASE_URL

Rule 15: Project-Specific Custom Rule Template

A starter template you can customize for any project.

# {Project Name} — Cursor Rules

## Stack
- Framework: Next.js 15 (App Router)
- Language: TypeScript (strict)
- Styling: Tailwind CSS + shadcn/ui
- Database: {Drizzle + Postgres / Prisma + Postgres / Convex / Supabase}
- Auth: {Auth.js / Clerk / Supabase Auth}
- Deployment: {Vercel / AWS / Railway}

## Architecture
- app/ — Routes and pages
- components/ — Reusable UI components
- components/ui/ — shadcn/ui primitives
- lib/ — Utility functions and shared logic
- drizzle/ — Database schema and migrations (if using Drizzle)
- public/ — Static assets

## Code Style
- {Your specific conventions here}

## Domain Rules
- {Business logic rules specific to your app}
- {e.g., "All prices are stored in cents as integers, never as floats"}
- {e.g., "User display names are always formatted as First L."}

## Do NOT
- {Things Cursor keeps doing wrong that you want to prevent}

How to Combine Rules for Your Project {#combining-rules}

You do not need all 15 rules. Pick the ones that match your stack and combine them into a single .cursorrules file. Here is what I use on a typical Next.js + Tailwind + shadcn + Drizzle project:

My recommended starter combo:

  • Rule 1 (App Router Foundation) — always
  • Rule 2 (Tailwind + cn()) — if using Tailwind
  • Rule 3 (TypeScript Strict) — always
  • Rule 5 (Component Patterns) — always
  • Rule 6 (Data Fetching) — always
  • Rule 13 (Performance) — always
  • Rule 14 (Environment Variables) — always

That gives you about 800 words of rules, well under the context limit. Add the database, auth, and testing rules as needed.

Pro tip: Put the rules in priority order. If Cursor hits its context limit, the rules at the top get priority. Framework conventions (Rule 1) and type safety (Rule 3) should always be first.

If you want to browse more Cursor rules for different stacks — Python, Go, Rust, Vue, Svelte, and more — the Skiln Cursor directory has thousands of community-contributed configurations organized by language and framework.


Frequently Asked Questions {#faq}

Where do I put the .cursorrules file?

In your project root, at the same level as package.json. Cursor automatically detects and reads it when you open the project. No configuration needed — just create the file and start coding.

Do .cursorrules work with Cursor's free plan?

Yes. The .cursorrules file works on all Cursor plans, including the free tier. The rules are processed locally as context — they do not require any special subscription or feature flag.

Can I use multiple .cursorrules files in a monorepo?

Not natively. Cursor reads the .cursorrules file from the root of the opened folder. In a monorepo, you have two options: put a combined rules file at the monorepo root covering all packages, or open individual packages as separate Cursor workspaces, each with their own .cursorrules.

How long should a .cursorrules file be?

Keep it under 2,000 words. Cursor includes the full rules file in its context window for every generation. Long rules eat into the context you need for your actual code. I find 500-1,000 words with a few code examples is the sweet spot — specific enough to be useful, short enough to leave room for code.

Do these rules work with other AI coding tools like Claude Code?

Not directly. Claude Code uses a different configuration system (CLAUDE.md files and custom skills). However, the conventions themselves — component patterns, data fetching approaches, TypeScript standards — transfer to any AI coding tool. You would just express them in that tool's configuration format. Skiln has resources for Claude Code skills and MCP servers that serve a similar purpose.


Frequently Asked Questions

Where do I put the .cursorrules file?
In your project root, at the same level as package.json. Cursor automatically detects and reads it when you open the project. No configuration needed — just create the file and start coding.
Do .cursorrules work with Cursor's free plan?
Yes. The .cursorrules file works on all Cursor plans, including the free tier. The rules are processed locally as context — they do not require any special subscription or feature flag.
Can I use multiple .cursorrules files in a monorepo?
Not natively. Cursor reads the .cursorrules file from the root of the opened folder. In a monorepo, you have two options: put a combined rules file at the monorepo root covering all packages, or open individual packages as separate Cursor workspaces, each with their own .cursorrules.
How long should a .cursorrules file be?
Keep it under 2,000 words. Cursor includes the full rules file in its context window for every generation. Long rules eat into the context you need for your actual code. 500-1,000 words with a few code examples is the sweet spot.
Do these rules work with other AI coding tools like Claude Code?
Not directly. Claude Code uses a different configuration system (CLAUDE.md files and custom skills). However, the conventions themselves transfer to any AI coding tool. Skiln has resources for Claude Code skills and MCP servers that serve a similar purpose.

Stay in the Loop

Join 1,000+ developers. Get the best new Skills & MCPs weekly.

No spam. Unsubscribe anytime.