AI Coding Rules for React and Next.js Projects
Ready-to-use AI coding rules for React and Next.js projects, covering component patterns, App Router conventions, server components, and testing.
AI Coding Rules for React and Next.js Projects
Generic AI coding rules get you halfway there. The other half is framework-specific: React has strict component patterns, Next.js has two routers with entirely different mental models, and the App Router introduced server components that fundamentally change how you think about data fetching.
Without specific rules, AI tools default to patterns that are outdated, inconsistent, or just wrong for your stack. You get Pages Router code in an App Router project. You get useEffect data fetching when you should be using async server components. You get export default everywhere when your team requires named exports.
This guide covers the rules you need for every layer of a React and Next.js project: components, routing, data fetching, styling, and testing. Each section includes copy-paste rules you can adapt for your own codebase.
If you're new to AI rules in general, start with The Complete Guide to Cursor Rules first, then come back here for React-specific patterns.
Routing: App Router vs Pages Router
The first and most important rule for any Next.js project is which router you're using. AI models are trained on years of Pages Router content, so they'll default to it unless you're explicit.
## Routing
- This project uses the Next.js App Router exclusively (src/app/)
- Do NOT generate any Pages Router code (no pages/ directory, no getServerSideProps, no getStaticProps)
- Do NOT use the `useRouter` from `next/router` — use `next/navigation` instead
- All routes are defined by folder structure under src/app/
- Use layout.tsx for shared UI, loading.tsx for Suspense boundaries, error.tsx for error states
This single rule eliminates the most common AI mistake in Next.js projects. Pair it with explicit file structure guidance:
## File structure
- Route handlers live in src/app/api/[route]/route.ts
- Page components live in src/app/[route]/page.tsx
- Shared components live in src/components/
- Server-only utilities live in src/lib/server/
- Client utilities (hooks, helpers) live in src/lib/client/
Without this rule, AI tools will sometimes generate a pages/ directory alongside your app/ directory, or mix navigation imports from both next/router and next/navigation. That breaks things in ways that are confusing to debug. Being explicit upfront saves time.
Server vs client components
Server components are the default in the App Router, but AI tools frequently add "use client" unnecessarily. Bad rules let this slide. Good rules teach the AI when each is appropriate.
## Server and client components
Default to server components. Only add `"use client"` when the component needs:
- React hooks (useState, useEffect, useRef, etc.)
- Browser-only APIs (window, document, localStorage)
- Event listeners (onClick, onChange, etc.)
- Third-party client-only libraries
Never add `"use client"` to:
- Layout components that just compose other components
- Components that only display static or async-fetched data
- Wrapper components that don't interact with the DOM
When you do add `"use client"`, push it as far down the component tree as possible. Extract the interactive part into a small client component rather than making a large component client-side.
Alongside this, explicitly describe your data fetching pattern:
## Data fetching
In server components, fetch data directly with async/await:
async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await db.query.products.findFirst({
where: eq(products.id, id),
});
return <ProductDetail product={product} />;
}
Do NOT use useEffect + useState for data that can be fetched server-side.
Do NOT use SWR or React Query unless explicitly needed for client-side refetching.
Use React's `cache()` to deduplicate requests within a render pass.
This matters because the default AI behavior is to reach for useEffect + useState for data fetching, which was the right pattern before the App Router. With server components, fetching data in the component function body is cleaner, faster, and avoids loading states for data that doesn't need them.
Component patterns
React component conventions vary wildly across codebases. Without explicit rules, AI tools will mix patterns: sometimes using default exports, sometimes named, sometimes arrow functions, sometimes function declarations.
Define what you want clearly:
## Component conventions
Use named exports for all components (not default exports):
export function UserCard({ user }: UserCardProps) { ... } // correct
export default function UserCard(...) { ... } // wrong
Use function declarations, not arrow function components:
export function Button(props: ButtonProps) { ... } // correct
export const Button = (props: ButtonProps) => { ... } // wrong
Co-locate prop types above the component:
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
}
export function Button({ label, onClick, variant = "primary" }: ButtonProps) {
...
}
One component per file. File name matches component name in kebab-case:
UserCard -> src/components/user-card.tsx
Composition over configuration
AI tools often generate overly complex components with too many props. A rule about composition helps:
## Component design
Prefer composition over configuration. Instead of a single component with 15 props,
compose smaller components together:
// Preferred
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>Content here</CardContent>
</Card>
// Avoid
<Card title="Title" content="Content here" headerVariant="large" />
Use React.PropsWithChildren or explicit children prop for components that wrap content.
The composition pattern also makes components easier to test. A CardHeader is independently testable. A 15-prop Card requires you to think through every combination.
API routes with route handlers
The App Router replaces API routes with route handlers. The pattern is different enough from Pages Router API routes that you need explicit rules:
## Route handlers (API routes)
Use the App Router route handler pattern in src/app/api/[path]/route.ts:
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
try {
const data = await fetchData();
return NextResponse.json({ data });
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
- Export named functions: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
- Always wrap in try/catch and return proper error responses
- Use NextRequest/NextResponse for convenience helpers, or the standard Web Request/Response
- Validate request bodies before processing
- Return consistent response shapes: { data: ... } for success, { error: ... } for errors
Styling rules
Styling is another area where AI tools drift without clear guidance. Whether you use Tailwind, CSS modules, or styled-components, spell it out:
## Styling
This project uses Tailwind CSS v4 exclusively.
- Do NOT use CSS modules, inline styles, or styled-components
- Use utility classes directly on JSX elements
- Use `cn()` (from src/lib/utils.ts) to merge conditional classes:
cn("base-class", isActive && "active-class", className)
- Use the design system tokens defined in your CSS theme (@theme) for colors, spacing, and typography
- Do NOT hardcode color values -- use the defined palette
- For responsive design, use Tailwind's mobile-first breakpoints: sm, md, lg, xl
If you're using shadcn/ui or a similar component library, add that context too:
## Component library
This project uses shadcn/ui. Before building a new component:
1. Check if a shadcn/ui component already exists for the use case
2. If so, import from src/components/ui/ (not from the package directly)
3. Customize via the className prop and cn() utility, not by modifying the component files
4. Only create custom components when no shadcn/ui equivalent exists
TypeScript rules for React
TypeScript has React-specific patterns that AI tools sometimes get wrong:
## TypeScript
Use strict TypeScript. tsconfig has "strict": true -- do not use `any`.
Type component props explicitly (never infer from usage):
interface Props { ... } // always define, never skip
For event handlers, use React's built-in event types:
onChange: React.ChangeEventHandler<HTMLInputElement>
onClick: React.MouseEventHandler<HTMLButtonElement>
For refs, type them precisely:
const inputRef = useRef<HTMLInputElement>(null);
For async server component return types, omit the annotation or use:
async function Page(): Promise<React.ReactNode> { ... }
For context, always provide a typed default or throw on missing context:
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
Strict TypeScript rules catch a lot of AI-generated errors before they make it to review. Untyped event handlers and inferred prop types are two of the most common sources of subtle bugs.
Testing patterns
Without testing rules, AI generates test files that don't match your conventions: wrong test runner, wrong assertion library, wrong file naming.
## Testing
Use Vitest and React Testing Library. Test files live alongside source files:
src/components/user-card.tsx
src/components/user-card.test.tsx
For component tests:
- Use `render` from @testing-library/react
- Query with semantic selectors: getByRole, getByLabelText, getByText
- Avoid getByTestId -- only use it when semantic queries are impractical
- Test behavior, not implementation details (no direct state inspection)
For server-side logic (utilities, API handlers):
- Use standard Vitest describe/it/expect blocks
- Mock external dependencies (db, fetch) with vi.mock()
- Test error paths, not just happy paths
Do NOT write tests that check implementation details (checking which setState was called, etc.).
Do NOT snapshot test UI components -- they make refactoring painful.
A complete rules file for a Next.js project
Putting it all together, here's a starter .cursorrules or Claude Code rules file for a typical Next.js 15 project:
# Project: [Your App Name]
## Stack
- Next.js 15, App Router (src/app/)
- TypeScript 5 (strict mode)
- Tailwind CSS v4
- Drizzle ORM + PostgreSQL
- Vitest + React Testing Library
- shadcn/ui components
## Routing
- App Router only. No Pages Router code.
- Use next/navigation (not next/router)
- Route handlers in src/app/api/[route]/route.ts
## Components
- Named exports only (no default exports)
- Function declarations (not arrow functions)
- Props interface above each component
- One component per file, kebab-case filenames
- Default to server components; use "use client" only when needed
## Data fetching
- Async server components for server-side data
- No useEffect for data that can be fetched at render time
- Use React cache() for deduplication
## Styling
- Tailwind utility classes only
- cn() from src/lib/utils for conditional classes
- Use design tokens, not hardcoded values
## TypeScript
- No `any` types
- React event handler types (React.MouseEventHandler, etc.)
- Typed refs with useRef<ElementType>(null)
## Testing
- Vitest + React Testing Library
- Test files alongside source files (.test.tsx)
- Semantic queries (getByRole, getByLabelText)
- Test behavior, not implementation
Why rules drift without tooling
Writing good rules is the hard part. The easy part is keeping them consistent. The frustrating part -- without tooling -- is that rules drift. One developer updates the testing section, another doesn't pull the change, and suddenly your team is generating tests with two different patterns.
This gets worse as your team grows. A single developer can keep their own rules file up to date. A team of ten, spread across multiple repositories, cannot. Someone is always working off a stale copy, and the rules that were carefully written during project setup slowly become disconnected from how the codebase actually works.
The answer is to treat your rules like any other shared dependency. You version them, publish them, and install them -- the same way you'd handle a shared ESLint config or TypeScript preset.
Keeping rules in sync across your team
This is exactly what localskills.sh is designed for. You publish your React/Next.js rules as a skill once, and everyone installs from the same source. When you update the rules -- say, when you migrate from Pages Router to App Router -- you publish a new version. Everyone pulls it with one command.
You can see how other teams have solved this in real-world cursorrules examples, and learn the principles behind good rules in AI coding rules best practices.
The format differences between tools (Cursor's .mdc, Claude Code's CLAUDE.md, Windsurf's .windsurfrules) are handled automatically. One skill installs into all of them:
localskills install your-team/nextjs-rules --target cursor claude windsurf
Once your rules are published, new developers on your team can get set up in seconds. No copying files, no checking the wiki, no asking which router you're using. See how to publish your first skill for a step-by-step walkthrough.
Ready to lock in your React and Next.js conventions across your entire team and every AI tool you use? Create your free account and publish your first skill today.
npm install -g @localskills/cli
localskills login
localskills publish