Next.js, React and TypeScript rules for AI coding agents
A coding agent that learned Next.js from older tutorials will often write getServerSideProps, import useRouter from next/router, or put "use client" on a whole layout to silence an error. None of that belongs in an App Router project. These rules are written for the App Router with React Server Components and strict TypeScript, and they spell out where the server and client boundary goes, how fetching and caching should work, and how mutations are handled.
The part teams tend to miss is security: the rules treat every "use server" function as a public POST endpoint that must parse its input with a schema and check the session and permission inside the action. On the React side they push the agent to derive values during render instead of syncing props into state with useEffect.
Folder: rules/nextjs-react-ts/ · nextjs-react-ts.mdc, CLAUDE.md, AGENTS.md
What the Next.js rules tell your agent
- Server and client componentsFiles under
app/are Server Components by default. Add"use client"to the smallest leaf that needs state, effects, event handlers or browser APIs. Never add it to a layout or page just to make an error go away. - Data fetching and cachingFetch in async Server Components or in a data-access module they call. Do not create a Route Handler only so your own page can
fetchit. - Server Actions and mutationsA
"use server"function is a public POST endpoint. Parse its input with a schema and check the session and permission inside the action, every time. - TypeScriptKeep
strict: true. No implicitany; when a type is truly unknown, useunknownand narrow it. - ReactDo not copy props into state and sync them with
useEffect. Derive the value during render.
First rule of each section in nextjs-react-ts.mdc, quoted as shipped.
Mistakes these rules are written to prevent
next/routerdoes not work in the App Router. ImportuseRouter,usePathnameanduseSearchParamsfromnext/navigation.getServerSideProps,getStaticPropsandgetInitialPropsdo nothing inapp/.- A client component that calls
useSearchParamson a statically rendered route must sit inside a<Suspense>boundary, or the production build fails.
Source: “Pitfalls to avoid”.
Checks the agent runs before it says done
Example commands from the Next.js file (swap in your repo's own):
- Type check:
npx tsc --noEmit - Lint:
npm run lint - Unit tests:
npm test - Build:
npx next build
Run the type check (npx tsc --noEmit), the project's ESLint command and the unit tests. Run next build when you changed routing, caching or a client/server boundary. Report what you ran and what failed. Never claim a build passes without running it.
Before and after: React: remove state that is synced with useEffect
Prompt card 04 · Stack: React (any framework) · Type: Refactor · Best model: a fast small model is usually enough
What the card is for
Replace useState + useEffect pairs that copy or compute data from props or other state. Derived values computed during render are always in sync, cause one render instead of two, and remove a class of stale-state bugs.
Worked example from the card
Before
function ContactList({ contacts, query }: { contacts: Contact[]; query: string }) {
const [visible, setVisible] = useState<Contact[]>([]);
const [count, setCount] = useState(0);
useEffect(() => {
const v = contacts.filter(c => c.name.toLowerCase().includes(query.toLowerCase()));
setVisible(v);
setCount(v.length);
}, [contacts, query]);
return <><p>{count} contacts</p>{visible.map(c => <Row key={c.id} contact={c} />)}</>;
}
Expected AI output (abridged)
- visible + effect: derived from contacts and query. Compute during render.
- count: derived from visible.length. Remove the state.
- Cost: filtering a few hundred items per keystroke is cheap; no useMemo needed.
If contacts can be in the tens of thousands and the compiler is off, wrap the filter in
useMemo keyed on [contacts, query].
- No effect remains; the first render now shows the right list (before, it showed
"0 contacts" for one frame).
After
function ContactList({ contacts, query }: { contacts: Contact[]; query: string }) {
const needle = query.toLowerCase();
const visible = contacts.filter(c => c.name.toLowerCase().includes(needle));
return (
<>
<p>{visible.length} contacts</p>
{visible.map(c => <Row key={c.id} contact={c} />)}
</>
);
}
Excerpt from card 04; the prompt itself is in the pack.
More Next.js prompt cards in the pack
- Card 01: Next.js: audit "use client" boundaries
- Card 02: Next.js: security review of Server Actions
- Card 03: TypeScript: replace
anyand unsafe casts with checked types
Using these rules in Cursor, Claude Code and AGENTS.md tools
Cursor
Put _base.mdc and nextjs-react-ts.mdc in .cursor/rules/. Globs: app/**/*.tsx, app/**/*.ts, src/**/*.tsx, src/**/*.ts, components/**/*.tsx, middleware.ts, proxy.ts, next.config.*.
Claude Code
Root CLAUDE.md, base first. Path-scoped variant .claude/rules/nextjs-react-ts.md for app/**/*.{ts,tsx}, src/**/*.{ts,tsx}, components/**/*.tsx; PostToolUse hook: npx prettier --write <file>.
AGENTS.md tools (Codex, Gemini CLI, Aider)
Base then stack in one AGENTS.md. Gemini CLI needs context.fileName; Aider needs --read AGENTS.md. Put this file in apps/web/ in a monorepo, or the repo root for a single app. Agents use the AGENTS.md nearest to the file being edited, so a nested copy overrides a root one for that subtree.