Stack conventions
Next.js (App Router)
Next.js App Router applications with Server Components, Server Actions and route handlers, in TypeScript.
Next.js App Router applications with Server Components, Server Actions and route handlers, in TypeScript.
Used for: nextjs.
Principles
- Follow the router the repo uses; for new work prefer the App Router (
app/) and keep Pages Router code (pages/) consistent with its own conventions. - Components are Server Components by default; add
'use client'only at the leaves that need state, effects, event handlers or browser APIs. - Fetch data on the server, close to where it is used, and pass serialisable props to client components; never pass functions, class instances or secrets across the boundary.
- Treat every Server Action and Route Handler as a public HTTP endpoint: authenticate, authorise and validate input inside it, every time.
- Be explicit about caching for the installed Next.js version (fetch
cache/next.revalidate,'use cache'where enabled) and revalidate (revalidatePath/revalidateTag) after mutations. - Keep server-only code out of client bundles: import
server-onlyin modules that touch secrets or the database; onlyNEXT_PUBLIC_variables reach the browser. - Use the file conventions for UX states:
loading.tsx,error.tsx,not-found.tsx, and Suspense boundaries for streaming. - Use framework primitives for performance:
next/link,next/image,next/font,next/script, and the Metadata API for SEO. - Keep components and hooks pure: same props, state and context give the same JSX; side effects belong in event handlers or effects.
- Follow the Rules of Hooks: call hooks only at the top level of components and custom hooks, never in conditions, loops or callbacks.
- Derive values during render instead of mirroring props or other state into new state; keep the minimal state that represents the UI.
- Colocate state with the component that uses it; lift it only to the closest common parent, and reach for context or a store only for truly shared data.
Conventions
Accessibility
- Set a unique, descriptive
<title>per route with the Metadata API and setlangon<html>in the root layout. - Use
next/linkfor navigation so links remain real anchors; manage focus after client-side route changes for dialogs and in-page updates. - Use native elements (
button,a,nav,main,ul) instead of clickabledivs. - Every form control has a programmatic label; every image has meaningful
altoralt=""if decorative. - Interactive custom widgets follow the WAI-ARIA Authoring Practices keyboard patterns and manage focus (dialogs trap and restore focus).
- Do not convey information by colour alone; maintain WCAG 2.2 AA contrast.
- Announce async status changes with a live region where the user would otherwise miss them.
- When the change renders UI, use semantic HTML elements and accessible names for controls.
Concurrency
- Request APIs (
cookies(),headers(),params,searchParams) are async in current versions — await them. - Deduplicate repeated reads within a request with React
cache()for non-fetch data sources. - Guard async effects against stale updates (abort with AbortController in the cleanup or ignore results after unmount).
- Use
useTransition/useDeferredValuefor expensive non-urgent updates rather than debouncing state manually. - Use async and await consistently; do not mix callbacks and promises in one flow.
- Bound parallel requests to external services and handle partial failure with Promise.allSettled where appropriate.
- Pass AbortSignal to long-running operations so callers can cancel them.
Dependencies
- Upgrade Next.js with the official codemods (
npx @next/codemod) and read the version upgrade guide; keepreact/react-domat the version Next.js requires. - Use the repo's component library and design tokens instead of adding new UI kits or ad-hoc styles.
- Keep
reactandreact-domversions identical. - Add packages with the repository's package manager and commit its lock file.
- Prefer packages that ship their own types; add @types packages as dev dependencies only.
- Do not add a dependency for a few lines of standard code.
Documentation
- Note the caching/revalidation choice of each data-mutating feature in code comments or the README.
- Document a component's props with TypeScript types and short JSDoc on non-obvious props; keep Storybook stories current where the repo uses them.
- Add a short comment where an effect, memo or ref exists for a non-obvious reason (a browser quirk, a performance fix).
- Document exported functions and types with TSDoc comments that state behaviour, parameters and thrown errors.
- Let types document shapes; comment the reasons behind non-obvious decisions.
- Update the README or docs when commands, configuration or public APIs change.
Errors
error.tsxmust be a Client Component; show a recovery action (reset) and log the error digest server-side.- Call
notFound()for missing resources rather than rendering an empty page; return typed error state from Server Actions instead of throwing for expected validation failures. - Render loading, empty and error states explicitly for every async view.
- Use error boundaries for render errors; handle event-handler and async errors where they happen.
- Throw Error objects (or subclasses), never strings or plain objects.
- Narrow caught values from unknown before reading their properties.
- Await or explicitly handle every promise; do not leave floating promises.
- Add context when re-throwing by using the cause option of Error.
Logging
- Log on the server only; never log session tokens or personal data; use
instrumentation.tsfor tracing/monitoring set-up. - No
console.logleft in components; report errors through the repo's monitoring hook. - Send unexpected errors from error boundaries and failed requests to the repo's error-monitoring client with the component and action as context, never user input or tokens.
- Use the repository's logger rather than console in application code.
- Log structured fields (ids, counts, durations) rather than whole objects.
- Never log tokens, passwords, cookies or personal data.
Naming
- Route segments are folder names in kebab-case; dynamic segments
[id], catch-all[...slug], route groups(group), private folders_components. - Special files keep framework names:
page.tsx,layout.tsx,route.ts,loading.tsx,error.tsx,not-found.tsx. - Components in PascalCase, one exported component per file named after it; hooks start with
use. - Event handler props are
onXxx, handler implementationshandleXxx. - Boolean props read as predicates:
isOpen,hasError,disabled. - Use camelCase for variables and functions, PascalCase for types, classes and enums, and UPPER_SNAKE_CASE for true constants.
- Do not prefix interfaces with I or types with T.
- Name files the way the repository already does (kebab-case or camelCase), consistently.
Performance
- Avoid request waterfalls: start independent fetches together; preload where the docs recommend.
- Keep
'use client'boundaries small to minimise shipped JavaScript; import heavy client libraries withnext/dynamicwhen not needed at first paint. - Choose static, dynamic or cached rendering per route deliberately; reading
cookies()/headers()makes a route dynamic. - Fix the cause (state too high, unstable props, oversized lists) before adding memoisation; profile with React DevTools Profiler.
- Virtualise long lists; lazy-load heavy routes and components with
lazy+Suspense. - Avoid creating new object/array/function props in hot paths when the child is memoised.
- Avoid blocking the event loop with synchronous I/O or heavy CPU work in request handlers.
- Run independent async operations concurrently with Promise.all, with a bound on concurrency.
Security
- Re-check authorisation in every Server Action, Route Handler and data-access function — layouts and middleware/proxy checks alone are not enough.
- Never put secrets in
NEXT_PUBLIC_variables or pass them as props to client components. - Configure
images.remotePatternsnarrowly; set security headers (CSP, frame-ancestors) innext.configor the proxy. - Never use
dangerouslySetInnerHTMLwith untrusted content; sanitise (e.g. DOMPurify) when HTML rendering is unavoidable. - Do not build
href/srcfrom user input without allowing only http(s) schemes (nojavascript:URLs). - Keep secrets out of client bundles; anything in the bundle is public.
- Treat URL parameters, API responses and storage as untrusted: validate them with a schema before use and never interpolate them into markup or
eval-like APIs (XSS). - Commit the lockfile and keep
npm auditclean of high-severity advisories for production dependencies.
Structure
- Colocate route-specific components under the route (private
_componentsfolder); shared UI in the repo's components directory. - Keep server-only modules (db, secrets, data access) in a dedicated folder that imports
server-only. - Edge/proxy (middleware) logic stays thin: redirects, rewrites, header checks — not data access or heavy auth logic.
- Group by feature (component, hook, styles, tests together) unless the repo uses another layout.
- Keep components small; extract subcomponents when JSX exceeds a screen or mixes concerns.
- Put pure helpers and types outside the component body so they are not recreated per render.
- Keep one responsibility per module and export only what other modules need.
- Use import type for imports used only as types.
Testing
- Unit test client components with Testing Library; test data-access and Server Action logic as plain async functions.
- Cover async Server Components and full flows with end-to-end tests (Playwright) against
next build && next start. - Done means tested: new or changed behaviour (a route, a Server Action, a cache rule) ships with a unit or end-to-end test in the same change.
- Test behaviour through the DOM with Testing Library; query by role and accessible name first (
getByRole('button', { name: /save/i })), then label text, and use test ids last. - Use
user-eventfor interactions andfindBy*/waitForfor async UI instead of manual timeouts. - Do not test implementation details (internal state, hook call counts, class names).
- Mock the network at the boundary (MSW or the repo's fake API), not React internals.
- Unit-test hooks and pure helpers directly; integration-test components with their children, router and a mocked API (MSW) rather than shallow rendering.
Review checklist
- Is
'use client'limited to components that need interactivity or browser APIs? - Are props crossing the server/client boundary serialisable and free of secrets?
- Does every Server Action and Route Handler authenticate, authorise and validate input?
- Are server-only modules guarded with
import 'server-only'? - Is the caching/revalidation behaviour of new data fetching explicit and correct for the installed version?
- Are mutations followed by revalidation or redirect?
- Are loading, error and not-found states handled with the file conventions or Suspense?
- Are request APIs (
cookies,headers,params,searchParams) awaited? - Are
next/link,next/imageand the Metadata API used instead of raw equivalents? - Are independent data requests started in parallel rather than sequentially?
- Are hooks called unconditionally at the top level, with complete dependency arrays?
- Is any state derivable from props or other state (and therefore redundant)?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
eslint |
lint | eslint | no |
prettier |
format | prettier | no |
tsc |
typecheck | tsc | no |
biome |
lint | biome | no |
npm-audit |
security | npm audit | no |
gitleaks |
security | gitleaks | no |