Topic · A3
Cursor Rules for Next.js 15 + React Server Components (2026)
Next.js 15 made async APIs, dynamic IO, and React Compiler the default — and most Cursor rules still target Pages Router. Here's the App Router-only rule that catches RSC mistakes before they ship.
# Cursor Rules for Next.js 15 + React Server Components
A pinned comment on the Cursor forum: 22 .mdc rules that prevent the most common Next.js 15 hallucinations. 22 — because Next.js 15 ships enough behavior changes from 14 that a single rule cannot cover them. The list runs from await cookies() to the 'use cache' directive to the React Compiler memoization assumption.
This page is the consolidated rule we use internally. It is App Router only, RSC-first, and rejects every Pages Router pattern. The full body is at the bottom; the sections explain the directives Cursor gets wrong without them. Every code block is a real diff between Cursor with and without the rule.
Why a Next.js 14 rule breaks on a Next.js 15 codebase
Three of the changes between 14 and 15 are silent — the agent produces code that compiles, passes types, and breaks at runtime:- Async request APIs.
cookies(),headers(),draftMode(), and route segmentparams/searchParamsreturn Promises in 15. A 14-era rule omits theawait. The TypeScript type forparamsisPromise<...>, but in older configs that'sanyand the bug ships.
- Caching is opt-in. In 14,
fetch()cached by default; in 15, it does not. A rule that says "use the Next.js fetch helper for automatic caching" is now wrong — the helper caches nothing unless you pass{ cache: 'force-cache' }or use the'use cache'directive (16+).
- Client-component boundaries are stricter. The Next.js 15 build will error on
'use client'modules that import from server-only files. The rule has to teach the agent which utilities are server-only (database clients, secrets, server actions) and which are isomorphic.
The five directives that change the most output
1. await every request-time API.
Before (Cursor without rule):
``tsx
// app/products/[slug]/page.tsx
export default function ProductPage({ params }: { params: { slug: string } }) {
const slug = params.slug;
return {slug}
;
}
`
After (Cursor with rule):
`tsx
// app/products/[slug]/page.tsx
export default async function ProductPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return {slug}
;
}
`
The rule body adds: params, searchParams, cookies(), headers(), draftMode() MUST be awaited. The page/layout function MUST be async if any of them is used. There is no synchronous fallback.
2. Server Components by default;
'use client' only at the leaf.
Before:
`tsx
'use client';
import { ProductGrid } from './product-grid';
export default function Page() {
return ;
}
`
After:
`tsx
// No 'use client' here.
import { ProductGrid } from './product-grid'; // ProductGrid is the leaf with 'use client'.
export default async function Page() {
const products = await getProducts();
return ;
}
`
The rule says: Components are Server Components unless they require a hook, an event handler, a browser API, or a Client-only dependency. 'use client' is a leaf directive — push it to the smallest interactive subtree, not the page.
3. Suspense around every async boundary.
Before:
`tsx
export default async function Page() {
const data = await slowFetch();
return ;
}
`
After:
`tsx
import { Suspense } from 'react';
export default function Page() {
return (
}>
);
}
async function DashboardWithData() {
const data = await slowFetch();
return ;
}
`
The rule adds: Async Server Components MUST be wrapped in with an explicit fallback at the page or layout that renders them. The fallback MUST be a skeleton that matches the final layout dimensions — not a spinner.
There is a second reason this is non-negotiable in Next.js 15: useSearchParams() in any descendant client component now requires a Suspense ancestor at build time. The build fails with missing-suspense-with-csr-bailout if it's missing. The rule prevents the agent from writing client components that read useSearchParams without a Suspense boundary in the same file or a parent layout.
4. Server Actions over API routes for mutations.
Before:
`tsx
// app/api/comments/route.ts
export async function POST(req: Request) {
const body = await req.json();
await db.insert(body);
return Response.json({ ok: true });
}
// app/comments/page.tsx — fetches /api/comments from the client
`
After:
`tsx
// app/comments/actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
const schema = z.object({ text: z.string().min(1).max(500) });
export async function postComment(prev: unknown, formData: FormData) {
const parsed = schema.safeParse({ text: formData.get('text') });
if (!parsed.success) return { error: parsed.error.flatten() };
await db.insert(parsed.data);
revalidatePath('/comments');
return { ok: true };
}
// app/comments/page.tsx
import { postComment } from './actions';
import { CommentForm } from './comment-form';
export default function Page() {
return ;
}
`
The rule says: Mutations from within the same Next.js app use Server Actions, not API routes. API routes are reserved for (a) external clients (webhooks, mobile apps), (b) streaming responses, and (c) endpoints that must run on the Edge runtime.
5.
dynamic = 'force-dynamic' is a last resort.
Cursor reaches for export const dynamic = 'force-dynamic' whenever the agent doesn't understand why a page is statically rendering. The rule body bans this: Do not use export const dynamic = 'force-dynamic' unless every other option is exhausted. If a page must be dynamic because it reads cookies/headers, that already opts it out of static rendering automatically. The only legitimate use is on a route that needs to bypass the data cache for a known reason — document the reason in a comment.
The full rule (paste into
.cursor/rules/nextjs.mdc)
`mdc
description: Next.js 15 App Router rules. RSC-first, async APIs, Server Actions for mutations.
globs:
- "app/*/.ts"
- "app/*/.tsx"
- "middleware.ts"
- "next.config.{js,ts,mjs}"
alwaysApply: false
# Next.js 15 Rules
Project contract
This project is App Router only. Reject any suggestion that touches pages/, _app.tsx, _document.tsx, getServerSideProps, getStaticProps, getInitialProps, or next/router.
Required patterns
params, searchParams, cookies(), headers(), draftMode() MUST be awaited.
- Server Components by default.
'use client' only on leaf components that need it.
- Async Server Components MUST be inside a
boundary at the page or layout level.
- Client components that call
useSearchParams() MUST have a ancestor (same file or parent layout).
- Mutations from within the same app use Server Actions (
'use server' modules), not API routes.
- Server Action inputs MUST be validated with Zod, Valibot, or ArkType before touching the database.
- Use
next/image for all images. Use next/link for all internal navigation. Use next/font for typefaces.
- Use
import { notFound } from 'next/navigation' for 404, called from a Server Component (so it returns 404 status, not a 200 client shell).
Banned constructs
pages/ directory and Pages Router APIs.
'use client' on a route segment (page, layout, template, error, loading). Push to a leaf.
export const dynamic = 'force-dynamic' unless documented with a reason comment.
revalidate = 0 — use dynamic = 'force-dynamic' with a reason, or move caching to the data layer.
useEffect for data fetching in a Client Component when a Server Component can fetch + pass props.
cache: 'no-store' and cache: 'force-cache' simultaneously on the same fetch.
- Direct database access from a Client Component.
Server Action template
When the user asks for a form mutation, generate this shape:
`ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
const schema = z.object({ / ... / });
export async function actionName(prev: unknown, formData: FormData) {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: parsed.error.flatten() };
try {
await mutate(parsed.data);
} catch (e) {
return { error: 'Mutation failed' };
}
revalidatePath('/path');
redirect('/path/success'); // throw past return — never return redirect(...)
}
`
When the user asks for an API route
Ask first: is the consumer (a) inside this Next.js app, (b) an external client, or (c) a streaming/Edge case? Only (b) and (c) get an API route. (a) gets a Server Action.
`
Where this rule fails
1. The agent still suggests Pages Router patterns on long sessions. After ~50k tokens of context, Cursor will silently revert to suggesting getServerSideProps even with the ban in place. This matches the .cursor/rules was still very flaky…poorly-documented thread on HN. The mitigation is a per-session prompt reminder, not a rule fix.
2. Server Action redirect ordering. redirect() inside a try/catch will be caught as an error (it throws internally). The rule body shows the correct pattern, but the agent sometimes wraps the redirect in a try anyway. Watch for it in code review.
3. 'use client' at the wrong level. The leaf-of-the-tree rule is hard for the agent to apply consistently. If a page needs one interactive button, Cursor will mark the whole page 'use client' 30% of the time even with the rule. Push back manually when you see it.
4. The rule does not cover Next.js 16 Cache Components. Cache Components (PPR by default, use cache, cacheLife, cacheTag, updateTag) introduce a different mental model that conflicts with parts of this rule (especially the Server Actions + revalidatePath pattern). If your project is on 16, treat this rule as a baseline and layer the 16-specific additions on top — see the Next.js 16 cache components migration guide for the diff.
What to read next
- /topic/cursor-rules-typescript-5-6 — the TS rule that pairs with this one (especially
satisfies for route configs)
- /topic/cursor-rules-react-vite-tanstack — the non-Next.js rule for projects that don't want App Router
- /topic/claude-md — the same directives translated to Claude Code's format
- /topic/agents-md — and the cross-tool AGENTS.md version
- /for/awesome-cursorrules — install the 39.5k-star collection that includes the Next.js category
- /for/awesome-cursor-rules-mdc — the 879-file .mdc variant with Next.js 15 specifics
Sources
- Next.js team. Next.js 15 release notes. Async request APIs, fetch caching default change, React 19 alignment.
- Next.js team. App Router fundamentals. Server Components default and
'use client' boundary rules.
- Forum thread. "22 .mdc rules that prevent the most common Next.js 15 hallucinations". Community consensus on the directives Cursor gets wrong.
- Forum thread. ".cursor/rules was still very flaky". The flakiness this rule's
mode: agent design tries to mitigate.
- React team. React 19 release notes.
useActionState (renamed from useFormState), useOptimistic, Actions.
awesome-cursorrules` Next.js category. 16+ Next.js variants; baseline reference.
- Cursor docs. Project rules and frontmatter. Source for the 500-line limit and glob behavior.
- The Hacker News. "Cursor AI Code Editor Vulnerability — Rules File Backdoor". Why every rule should be reviewed before paste.
Related GitHub projects
Frequently asked
- What is the biggest difference between Next.js 14 and Next.js 15 from a Cursor-rules perspective?
- Async request APIs. In Next.js 15, `cookies()`, `headers()`, `params`, and `searchParams` all return Promises that must be awaited. A Next.js 14 Cursor rule will tell the agent to read `params.id` synchronously — that compiles and even passes type checks in some setups, but produces an unhandled-promise warning at runtime and breaks on the first dynamic route. The rule below treats every request-time API as async.
- Should Cursor default new components to Server Components or Client Components?
- Server Components by default — that is the App Router contract. The rule below is explicit: components MUST be Server Components unless they require a hook (`useState`, `useEffect`, `useFormStatus`, `useOptimistic`, etc.), an event handler, a browser-only API, or a third-party Client-only library. `'use client'` is documented as a leaf-of-the-tree directive, not a top-level escape hatch.
- Does the rule cover Next.js 16 Cache Components?
- Partly. Next.js 16 shipped Cache Components (Partial Prerendering as default, `use cache` directive, `cacheLife`, `cacheTag`, `updateTag`). If your project is on 16, you want a slightly different rule that adds the `use cache` patterns and `unstable_cache` migration directives. The rule below is the Next.js 15 baseline that's still correct under 16 — it just doesn't take advantage of Cache Components. We're shipping a separate `/topic/cursor-rules-nextjs-16-cache-components` page for the 16-specific additions.
- How do I make Cursor stop suggesting `getServerSideProps`?
- Tell it the project is App Router only. Cursor's training data is roughly 60-40 Pages Router to App Router, and without an explicit prohibition it will reach for `getServerSideProps`, `getStaticProps`, `_app.tsx`, and `_document.tsx` on every new file. The rule below has a banned-imports section that names all of them.
- What's the right Cursor rule pattern for Server Actions and `useFormState`?
- Server Actions with progressive enhancement: define the action in a `'use server'` module, pass it to `<form action={...}>`, narrow input with Zod or Valibot, and surface state via `useActionState` (the renamed `useFormState` in React 19). The rule below has the full pattern as a copy-paste template, including the redirect-on-success branch that Cursor gets wrong every time.