nextjs-architect · v1.0.0 · 2026-05-22 · sha256 8e22d98f9ba6aa94
nextjs-architect v1.0.0A
Immutable. This exact content never changes and is served at /api/v1/blob/8e22d98f9ba6aa94.
---
name: nextjs-architect
version: 1.0.0
description: Next.js 16 standards — App Router only, server components by default with explicit `"use client"` boundaries, server actions for mutations, streaming Suspense, edge vs node runtime, Image/Font/Metadata APIs. Pairs with react-architect. Use when scaffolding or reviewing a Next.js app or auditing server/client boundaries.
---
# Next.js Architecture
Targets **Next.js 16** with **React 19**, **TypeScript strict**, **App Router only**. Builds on [react-architect](../react-architect/SKILL.md) for client-side patterns; this skill covers the Next-specific layers — server components, server actions, routing, data fetching, edge vs node, deployment. See [STACK.md](STACK.md) for pinned dependencies.
## 1. App Router only
The Pages Router is legacy. New code lives in `app/`; if you inherit a Pages Router app, **don't mix** unless mid-migration.
```
app/
├── layout.tsx # root layout (html/body, Providers)
├── page.tsx # /
├── loading.tsx # default loading UI for /
├── error.tsx # error UI for /
├── not-found.tsx # 404 UI
├── (auth)/ # route group — doesn't add to URL
│ ├── login/page.tsx # /login
│ └── signup/page.tsx # /signup
├── dashboard/
│ ├── layout.tsx # nested layout for /dashboard/*
│ ├── page.tsx # /dashboard
│ ├── @analytics/page.tsx # parallel route slot
│ └── users/
│ ├── page.tsx # /dashboard/users
│ └── [id]/
│ ├── page.tsx # /dashboard/users/[id]
│ └── edit/page.tsx # /dashboard/users/[id]/edit
└── api/
└── webhooks/
└── stripe/route.ts # POST /api/webhooks/stripe
```
- **`page.tsx`** = the route's UI. **`layout.tsx`** wraps it. Both default to server components.
- **`loading.tsx`** wraps the route in Suspense automatically.
- **`error.tsx`** is a client component that wraps the route in an ErrorBoundary.
- **Route groups (`(name)`)** organize without adding URL segments.
- **`@slot`** is a parallel route — independent loading/error states, rendered into named slots in the parent layout.
## 2. Server components by default, client when needed
Every component is a **server component** unless you opt out with `"use client"`. The choice belongs to the leaf component that needs the client behavior — not the root.
### When to stay server
- Pure rendering — no state, no effects, no event handlers.
- Data fetching — `await db.query(...)`, `await fetch(...)`.
- Secrets / server-only logic — `process.env.STRIPE_SECRET_KEY` *only* loaded in server components.
- Markdown rendering, syntax highlighting, large dependencies you don't want shipped to the browser.
### When to flip to `"use client"`
- `useState`, `useEffect`, `useReducer`, any other hook.
- Browser-only APIs (`window`, `localStorage`, `IntersectionObserver`).
- Event handlers (`onClick`, `onChange`, etc.) — must be in a client boundary.
- Third-party libraries that use any of the above (most chart libraries, animation libraries).
### The boundary rule
Skeleton: [RECIPES.md § Server / client boundary](RECIPES.md#server--client-boundary).
- **Server passes data to client as props** — props must be JSON-serializable. Pass IDs, not class instances; pass plain objects, not Date (use ISO strings).
- **One `"use client"` directive** at the top of a file makes that *file* and everything imported into it run client-side. Hoist the directive to the smallest leaf.
- **Don't nest client → server.** Server components can be rendered inside client trees only as `children` props (slot pattern).
## 3. Data fetching
### Reads — direct from server components (hybrid default)
Per the project choice: **direct DB access from RSC for reads**, API routes for writes. Cleaner UX (single round-trip), type-safe end-to-end. Skeleton: [RECIPES.md § Read directly from a server component](RECIPES.md#read-directly-from-a-server-component).
- **`getUser` lives in a server-only module** (`@/lib/users.server.ts` or marked with the `"server-only"` import). Prevents accidental bundling of DB code into the client.
- **Use [sql-architect](../../databases/sql-architect/SKILL.md)'s `psycopg + .sql` pattern** when the project's backend convention matches; otherwise use the project's existing data layer.
- **`notFound()`** triggers the closest `not-found.tsx` — proper 404 with the right HTTP status.
**When to flip to API routes for reads:** any of the following — and document the choice per project:
- You want a separate microservice topology (Next is the frontend BFF, backend is its own service).
- The same data is consumed by mobile clients or other web frontends; one HTTP API beats two implementations.
- Auth requirements force the boundary (e.g. backend has access controls direct DB calls can't replicate).
### Writes — server actions
Server actions are the canonical mutation pattern. No client-side fetch, no manual JSON handling. Skeleton: [RECIPES.md § Server action with Zod validation + form binding](RECIPES.md#server-action-with-zod-validation--form-binding).
- **`"use server"`** at the top of a file marks every export as a server action — accessible from client components via `import`.
- **Always re-authenticate inside the action.** Don't trust the calling context; verify the session.
- **Validate with Zod** at the boundary — same discipline as REST handlers per [rest-api-architect §7](../../protocols/rest-api-architect/SKILL.md#7-error-contracts--rfc-7807-problem-details).
- **`revalidatePath` / `revalidateTag`** to refresh server-rendered data after a mutation. Without this, the user sees stale state.
- **Return discriminated unions** (`{ok: true, ...} | {ok: false, ...}`) so the client renders success vs. errors based on the field.
### TanStack Query — when?
The [react-architect §5](../react-architect/SKILL.md#5-state-management--three-layers) recommendation stands inside client components — but in Next.js the trade-off shifts:
- **Server components fetch on the server.** TanStack Query's "server state cache" benefits don't apply when the page is server-rendered fresh.
- **TanStack Query earns its place in Next when:** there's substantial client-side data fetching (autocomplete, real-time updates, infinite scroll, polling), or you want optimistic UI for mutations beyond what server actions give.
## 4. Streaming with Suspense
Next App Router streams the HTML as RSC chunks resolve. Pair with `<Suspense>` for granular per-region loading. Skeleton: [RECIPES.md § Per-region streaming with Suspense](RECIPES.md#per-region-streaming-with-suspense).
- **`loading.tsx` is a route-level Suspense.** Per-region Suspense gives finer-grained streaming.
- **`error.tsx` is a route-level ErrorBoundary.** Inline `<ErrorBoundary>` (from `next/error-boundary` or a custom one) for finer scoping.
- **Don't over-stream.** A page with 12 Suspense boundaries appears janky as chunks fill in. Target the genuinely slow regions.
## 5. Edge vs Node runtime
Each route can declare its runtime:
```tsx
// app/api/quick-check/route.ts
export const runtime = "edge";
export async function GET(req: Request) {
return Response.json({ ok: true });
}
```
- **Default `node`.** Full Node API, npm packages with native modules, deepest compatibility.
- **`runtime = "edge"`** for low-latency, geographic-edge routes — auth verification, redirects, feature flags, A/B routing.
- **Edge restrictions:** no Node-only modules (`fs`, `child_process`), no native bindings, smaller bundle limit (~1 MB).
- **DB drivers vary.** Standard Postgres drivers don't work on edge; use HTTP-based drivers (Neon serverless driver, Cloudflare D1, Turso libsql) when running on edge.
## 6. Metadata, Images, Fonts
### Metadata
```tsx
// app/dashboard/users/[id]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params;
const user = await getUser(id);
return {
title: user ? `${user.name} — Users` : "User not found",
description: user?.bio,
openGraph: { images: user?.avatarUrl ? [user.avatarUrl] : [] },
};
}
```
- **Per-route `generateMetadata`** — async, runs on the server, has access to params.
- **Root `metadata` export** in `app/layout.tsx` for defaults (title template, OG defaults).
- **Don't hand-write `<head>`** — Next manages it for SEO + social embed correctness.
### `next/image`
- **Always `next/image`**, never `<img>` in App Router code. Handles responsive sizes, modern formats (AVIF/WebP), lazy loading, preventing layout shift.
- **Set `width` and `height`** (or `fill` with a sized parent). Prevents CLS.
- **`priority` only on above-the-fold images.** Every image marked priority defeats the optimization.
- **Remote images need `remotePatterns`** in `next.config.ts` — strict allow-list for security.
### `next/font`
- **`next/font/google`** for Google Fonts; subsets + variable axes pinned at build time, no external request.
- **`next/font/local`** for self-hosted fonts.
- **Both eliminate FOIT/FOUT.** Don't use `@import` or `<link rel="stylesheet">` for fonts in App Router code.
## 7. API routes — when
Server actions cover most mutations. API routes (`app/api/.../route.ts`) earn their place for:
- **Webhooks** — external services (Stripe, GitHub, etc.) POST to your API.
- **External clients** — mobile, public API, partner integrations.
- **Streaming responses** — server-sent events, NDJSON streams.
- **Non-React consumers** — anything that isn't a form submission from your own UI.
Inside API routes, follow [rest-api-architect](../../protocols/rest-api-architect/SKILL.md) — same conventions for status codes, errors (RFC 7807), versioning, idempotency.
## 8. Auth
- **Session-based for first-party apps** — `next-auth` (Auth.js v5) or a hand-rolled cookie session. `httpOnly`, `Secure`, `SameSite=Lax` cookies.
- **Server-side session check** in every protected `layout.tsx` or via middleware. Never trust the client.
- **`middleware.ts`** at the project root runs on every matching route — auth redirects, locale detection, feature flags. Edge runtime.
- **Server actions re-verify** the session — middleware doesn't reach into action handlers reliably.
- **External IdP** (Auth0, Cognito, Clerk) is the typical upgrade path; same [rest-api-architect §11](../../protocols/rest-api-architect/SKILL.md#11-authentication-patterns) trade-offs apply.
## 9. Caching
Next has many cache layers. Be deliberate.
| Cache | Default | Override |
|---|---|---|
| **`fetch()` data cache** | Off by default in Next 16 | `fetch(url, { cache: "force-cache", next: { revalidate: 60 } })` |
| **Full route cache** | Static routes cached at build; dynamic routes per-request | `export const dynamic = "force-static"` / `"force-dynamic"` |
| **Router cache (client)** | Caches RSC payloads for 30s | `router.refresh()` to bust |
| **`unstable_cache`** | Wrap arbitrary data functions with TTL + tag-based invalidation | Use tags for `revalidateTag()` |
- **Don't cache user-specific data globally.** Use `cookies()` / `headers()` to opt out of caching, or scope the cache to the user.
- **`revalidateTag` after mutations** so server-rendered data refreshes when the underlying data changes.
- **Profile before tuning.** Most apps don't need cache surgery; trust the defaults until something's measurably slow.
## 10. Testing
Same approach as [react-architect §10](../react-architect/SKILL.md#10-testing) plus Next-specific:
- **Component tests** with Vitest + React Testing Library. Server components are rendered to strings by Next — testable in isolation as async functions returning JSX.
- **Server action tests** are unit tests of the action function. Call it with a `FormData` object, assert the return.
- **Playwright** for end-to-end against `next start` (built app) or `next dev` (in CI: build then start).
- **API route tests** invoke the route handler directly with a `Request` object; no HTTP server needed.
## 11. Deployment
- **Vercel** is the path of least resistance — first-class App Router, edge, streaming, ISR.
- **Self-host** via `next start` behind a reverse proxy, or `output: "standalone"` for a tiny Docker image (per [docker-architect](../../infra/docker-architect/SKILL.md)).
- **Static export** (`output: "export"`) when there's no dynamic server logic — drops half of Next's features but enables static hosting.
- **Always set the `NEXT_PUBLIC_*` env conventions** correctly — anything not prefixed is server-only.
## 12. Cross-skill ties
- [react-architect](../react-architect/SKILL.md) — client patterns inside `"use client"` components.
- [ui-ux-architect](../../frontend/ui-ux-architect/SKILL.md) — Radix + Tailwind + a11y; design system tokens.
- [rest-api-architect](../../protocols/rest-api-architect/SKILL.md) — API routes follow the same conventions as backend REST services.
- [sql-architect](../../databases/sql-architect/SKILL.md) — direct DB access from RSC uses the same pattern (raw SQL + parameter binding); use the project's existing data layer if different.
- [security-reviewer](../../quality/security-reviewer/SKILL.md) — server actions and API routes are the new attack surface; same audit list applies.
- [observability-architect](../../infra/observability-architect/SKILL.md) — OpenTelemetry instrumentation via the `@vercel/otel` or generic OTel SDK; trace_id propagated through RSC boundaries.