Next.js (App Router) Conventions
Rules for building Next.js web applications with the App Router, consuming the shared Rails API backend.
Stack
| Concern |
Library |
| Framework |
Next.js 15+ (App Router) — Server Components by default. React 19 minimum |
| Styling |
Tailwind CSS (same conventions as the std-reactjs skill; use cn() for conditional classes) |
| Server state |
TanStack Query — Client Components only |
| Client state |
Zustand — Client Components only |
| HTTP |
axios (Rails API client) for client + server actions; fetch for cacheable server reads |
| Forms |
react-hook-form + zod (Client Components) |
| i18n |
next-intl or react-i18next |
| Testing |
Vitest + React Testing Library + MSW |
CSS Modules only for animations Tailwind cannot express.
This targets Next.js 15+ — and 14 answers differently
The version is part of the convention (std-infrastructure: pin every version). Next.js 15
changed caching defaults and request APIs, so the same code behaves differently on 14 — and
guidance that does not say which major it means is guidance you cannot check.
|
Next.js 14 |
Next.js 15+ (this repo) |
fetch |
cached by default |
not cached by default — opt in with cache: 'force-cache' |
GET Route Handlers |
cached by default |
not cached — opt in with export const dynamic = 'force-static' |
| Client Router Cache |
page segments reused on <Link> nav |
not reused (back/forward and shared layouts still are); tune via experimental.staleTimes |
cookies, headers, draftMode |
synchronous |
async — await cookies() |
params, searchParams |
plain objects |
Promises — await params |
Two consequences that shape every example in this skill:
params: Promise<{…}> and await cookies() are not style — they are required. Sync usage
is a 14-ism that warns in dev and breaks.
- Never rely on the
fetch default. Since 15 makes uncached the default and 14 made cached
the default, any code whose correctness depends on the default is a version bug waiting to
happen. State intent explicitly — next: { revalidate, tags } or cache: 'force-cache' — which
is why references/caching.md always does, and why it reads as verbose. That verbosity is the
point.
Upgrading from 14: npx @next/codemod@canary upgrade latest handles the async-API migration.
Project Structure
app/ # Routes: layout.tsx, page.tsx, loading.tsx, error.tsx, not-found.tsx
(auth)/ (dashboard)/ # Route groups
api/ # Route Handlers — BFF/webhooks/health only, never a second API
src/
components/ui|forms/ # Design system primitives, form components
actions/ # Server actions (use-case layer for mutations)
hooks/ stores/ # Client hooks, Zustand stores
api/ # Rails API client
domain/ types/ lib/ i18n/
middleware.ts # Edge middleware (auth gate, locale, redirects)
tests/
Core Rules
Server vs Client Components
- Every component is a Server Component by default — no directive needed. Use for data fetching,
static rendering, SEO-critical content, layouts.
- Add
'use client' only for interactivity: state, event handlers, browser APIs, Zustand,
TanStack Query. Keep Client Components small and leaf-level.
- Never add
'use client' to a page or layout file — extract the interactive part instead.
- Server Components can
await directly; they cannot use hooks, state, or event handlers.
// app/orders/page.tsx — Server Component fetches, Client Component renders interaction
import { OrderTable } from '@/components/OrderTable'; // 'use client'
export default async function OrdersPage() {
const orders = await fetchOrders();
return <OrderTable initialData={orders} />;
}
Data fetching
- Fetch in Server Components with
async/await. Parallelize independent reads with Promise.all.
- TanStack Query only when you need polling, infinite scroll, or optimistic updates — seed it with
server data via
initialData so there is no loading flash.
- ISR:
export const revalidate = N. Static: nothing, or dynamic = 'force-static'. Per-request:
dynamic = 'force-dynamic'. On-demand: revalidatePath() / revalidateTag().
Server actions
- Always validate input with zod — a server action is a public endpoint.
- Always authorize inside the action; never take identity from the form payload.
- Always
revalidatePath/revalidateTag after a successful mutation.
- Return serializable data only. Never return a raw error — catch, log, return a user-safe message.
- Prefer progressive enhancement:
<form action={formAction}> + useActionState.
Metadata
- Every page exports
metadata or generateMetadata. Use generateMetadata when the title
depends on fetched data. Set a canonical URL to avoid duplicate content.
Middleware
- Auth redirects, locale detection, A/B bucketing, rate limiting. Runs on the Edge Runtime —
keep it lightweight, no data fetching, always set a
matcher. It is a coarse gate, not the
security boundary.
Performance
- Prefer Server Components — less client JS.
next/image for every image; next/link for every internal link.
- Wrap slow data-fetching regions in
<Suspense> to stream the shell first.
- Audit client chunks with
@next/bundle-analyzer.
Anti-Patterns to Avoid
'use client' on a page or layout file.
useEffect for data fetching in pages.
- Importing server-only code in Client Components (mark those modules
import 'server-only').
- Missing
loading.tsx / error.tsx boundaries (error.tsx must be a Client Component).
<img> instead of next/image; <a> instead of next/link.
- Missing
metadata export.
- Server actions without input validation or without revalidation.
- A secret behind a
NEXT_PUBLIC_ prefix.
- Rebuilding the Rails API inside
app/api.
Deep guides (read on demand, do not preload)
- Server/Client boundary, composition, streaming,
<Suspense>, error boundaries → references/rendering.md
- Server actions: validation, forms, optimistic UI, redirects, testing →
references/server-actions.md
- Caching, ISR,
revalidateTag vs revalidatePath, request dedupe → references/caching.md
- Edge middleware, SEO/
generateMetadata/sitemaps, Vercel & ECS deploy → references/middleware-seo-deploy.md
1---2name: std-nextjs3description: Next.js App Router conventions — Server Components, server actions, ISR/SSG, Vercel. Use when building Next.js pages, layouts, or server actions.4paths:5 - "**/next.config.*"6 - "**/app/**/*.tsx"7 - "**/app/**/*.jsx"8 - "**/src/app/**/*.ts"9 - "**/src/app/**/*.tsx"10 - "**/middleware.ts"11---1213# Next.js (App Router) Conventions1415Rules for building Next.js web applications with the App Router, consuming the shared Rails API backend.1617## Stack1819| Concern | Library |20|---------|---------|21| Framework | **Next.js 15+** (App Router) — Server Components by default. React 19 minimum |22| Styling | Tailwind CSS (same conventions as the `std-reactjs` skill; use `cn()` for conditional classes) |23| Server state | TanStack Query — Client Components only |24| Client state | Zustand — Client Components only |25| HTTP | axios (Rails API client) for client + server actions; `fetch` for cacheable server reads |26| Forms | react-hook-form + zod (Client Components) |27| i18n | next-intl or react-i18next |28| Testing | Vitest + React Testing Library + MSW |2930CSS Modules only for animations Tailwind cannot express.3132## This targets Next.js 15+ — and 14 answers differently3334The version is part of the convention (`std-infrastructure`: *pin every version*). Next.js 1535changed **caching defaults and request APIs**, so the same code behaves differently on 14 — and36guidance that does not say which major it means is guidance you cannot check.3738| | Next.js 14 | **Next.js 15+ (this repo)** |39|---|---|---|40| `fetch` | cached by default | **not cached by default** — opt in with `cache: 'force-cache'` |41| `GET` Route Handlers | cached by default | **not cached** — opt in with `export const dynamic = 'force-static'` |42| Client Router Cache | page segments reused on `<Link>` nav | **not reused** (back/forward and shared layouts still are); tune via `experimental.staleTimes` |43| `cookies`, `headers`, `draftMode` | synchronous | **async — `await cookies()`** |44| `params`, `searchParams` | plain objects | **Promises — `await params`** |4546Two consequences that shape every example in this skill:4748- **`params: Promise<{…}>` and `await cookies()` are not style — they are required.** Sync usage49 is a 14-ism that warns in dev and breaks.50- **Never rely on the `fetch` default.** Since 15 makes uncached the default and 14 made cached51 the default, *any* code whose correctness depends on the default is a version bug waiting to52 happen. State intent explicitly — `next: { revalidate, tags }` or `cache: 'force-cache'` — which53 is why `references/caching.md` always does, and why it reads as verbose. That verbosity is the54 point.5556Upgrading from 14: `npx @next/codemod@canary upgrade latest` handles the async-API migration.5758## Project Structure5960```61app/ # Routes: layout.tsx, page.tsx, loading.tsx, error.tsx, not-found.tsx62 (auth)/ (dashboard)/ # Route groups63 api/ # Route Handlers — BFF/webhooks/health only, never a second API64src/65 components/ui|forms/ # Design system primitives, form components66 actions/ # Server actions (use-case layer for mutations)67 hooks/ stores/ # Client hooks, Zustand stores68 api/ # Rails API client69 domain/ types/ lib/ i18n/70middleware.ts # Edge middleware (auth gate, locale, redirects)71tests/72```7374## Core Rules7576### Server vs Client Components77- Every component is a Server Component by default — no directive needed. Use for data fetching,78 static rendering, SEO-critical content, layouts.79- Add `'use client'` only for interactivity: state, event handlers, browser APIs, Zustand,80 TanStack Query. Keep Client Components small and leaf-level.81- **Never add `'use client'` to a page or layout file** — extract the interactive part instead.82- Server Components can `await` directly; they cannot use hooks, state, or event handlers.8384```tsx85// app/orders/page.tsx — Server Component fetches, Client Component renders interaction86import { OrderTable } from '@/components/OrderTable'; // 'use client'8788export default async function OrdersPage() {89 const orders = await fetchOrders();90 return <OrderTable initialData={orders} />;91}92```9394### Data fetching95- Fetch in Server Components with `async/await`. Parallelize independent reads with `Promise.all`.96- TanStack Query only when you need polling, infinite scroll, or optimistic updates — seed it with97 server data via `initialData` so there is no loading flash.98- ISR: `export const revalidate = N`. Static: nothing, or `dynamic = 'force-static'`. Per-request:99 `dynamic = 'force-dynamic'`. On-demand: `revalidatePath()` / `revalidateTag()`.100101### Server actions102- **Always validate input with zod** — a server action is a public endpoint.103- **Always authorize inside the action**; never take identity from the form payload.104- **Always `revalidatePath`/`revalidateTag`** after a successful mutation.105- Return serializable data only. Never return a raw error — catch, log, return a user-safe message.106- Prefer progressive enhancement: `<form action={formAction}>` + `useActionState`.107108### Metadata109- **Every page exports `metadata` or `generateMetadata`.** Use `generateMetadata` when the title110 depends on fetched data. Set a canonical URL to avoid duplicate content.111112### Middleware113- Auth redirects, locale detection, A/B bucketing, rate limiting. Runs on the Edge Runtime —114 keep it lightweight, no data fetching, always set a `matcher`. It is a coarse gate, not the115 security boundary.116117### Performance118- Prefer Server Components — less client JS.119- `next/image` for every image; `next/link` for every internal link.120- Wrap slow data-fetching regions in `<Suspense>` to stream the shell first.121- Audit client chunks with `@next/bundle-analyzer`.122123## Anti-Patterns to Avoid124125- `'use client'` on a page or layout file.126- `useEffect` for data fetching in pages.127- Importing server-only code in Client Components (mark those modules `import 'server-only'`).128- Missing `loading.tsx` / `error.tsx` boundaries (`error.tsx` must be a Client Component).129- `<img>` instead of `next/image`; `<a>` instead of `next/link`.130- Missing `metadata` export.131- Server actions without input validation or without revalidation.132- A secret behind a `NEXT_PUBLIC_` prefix.133- Rebuilding the Rails API inside `app/api`.134135## Deep guides (read on demand, do not preload)136137- Server/Client boundary, composition, streaming, `<Suspense>`, error boundaries → `references/rendering.md`138- Server actions: validation, forms, optimistic UI, redirects, testing → `references/server-actions.md`139- Caching, ISR, `revalidateTag` vs `revalidatePath`, request dedupe → `references/caching.md`140- Edge middleware, SEO/`generateMetadata`/sitemaps, Vercel & ECS deploy → `references/middleware-seo-deploy.md`