Use when building, editing, or debugging a Next.js App Router app (Next 15/16, React 19) — anything involving the `app/` route tree, Server Components vs `'use client'`, Server Actions, Route Handlers, the fetch cache, `middleware.ts`, SEO/metadata, an Encore.ts (or other) backend client, a Turborepo monorepo, or Vercel/Docker deployment. Produces correct RSC boundaries, non-stale caching, verified UI. Grounded in the Next.js 16 App Router canon; extends with the 8syncdev repo patterns + omp-...
Installs into .claude/skills of the current project.
Are you the author of Nextjs App?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/8-sync-dev-nextjs-app)
---
name: nextjs-app
description: Use when building, editing, or debugging a Next.js App Router app (Next 15/16, React 19) — anything involving the `app/` route tree, Server Components vs `'use client'`, Server Actions, Route Handlers, the fetch cache, `middleware.ts`, SEO/metadata, an Encore.ts (or other) backend client, a Turborepo monorepo, or Vercel/Docker deployment. Produces correct RSC boundaries, non-stale caching, verified UI. Grounded in the Next.js 16 App Router canon; extends with the 8syncdev repo patterns + omp-native tooling (codegraph · codebase-memory · serena · engine_* · browser).
locked: true
---
# nextjs-app — Next.js 16 App Router, done right
You are working on a Next.js **App Router** project (Next 15/16.x, React 19). The App Router is not the Pages Router: the failure modes here are *boundary placement* (RSC vs Client), *silent stale data* (the fetch cache), and *invalidation that never fires*. This skill makes those load-bearing decisions explicit and grounds every step in a real omp tool.
> Canonical base: `references/base.md` (Next.js 16 mental model + the cache trap).
> Opinionated patterns: `references/patterns.md` (real `repo:path` citations from `8syncdev/crm-pro-ai`, `8syncdev/8syncdev-pro-v2`, `8syncdev/8sync-verse`).
## When to use
- Editing any file under `app/` (or `src/app/`): pages, layouts, route groups, `loading.tsx`/`error.tsx`/`not-found.tsx`.
- Adding a Server Action (`'use server'`) or a Route Handler (`app/api/*/route.ts`).
- Anything touching **caching**: `fetch` options, `unstable_cache`, `revalidatePath`/`revalidateTag`, ISR, `cacheComponents`/PPR.
- Auth/redirect/rewrite logic → `middleware.ts` (Edge runtime).
- Wiring an external backend (Encore.ts generated client, REST, GraphQL).
- Turborepo/Bun monorepo with multiple Next apps sharing `packages/*`.
- Deploying: Vercel multi-project, self-host Docker (`output: 'standalone'`), or static export (`output: 'export'`).
## When NOT to use
- **Pages Router** (`pages/` directory) — legacy. Only engage if the repo genuinely uses it.
- **Migrating Pages → App** — separate task; this skill assumes App Router already.
- Choosing a CSS framework (Tailwind/CSS Modules/unocss) — out of scope; follow the repo's existing choice.
## Procedure
Every step names a real omp tool. Do not improvise alternatives.
### 0. Ground yourself before editing — map the route tree, do not guess
You NEVER open `app/` files hoping. Before editing:
1. **codegraph** (semantic index, ~35% token save vs grep): `codegraph index .` then `codegraph context "route tree, middleware, server actions"`. Locate routes, actions, and the backend client in one call.
2. Only if **codebase-memory** is enabled (`8sync harness cbm on`) and the project is indexed, call `mcp__codebase_memory_mcp_get_architecture` (or `search_graph` for a symbol, `trace_path` for a call chain RSC→action→backend). For per-file symbols use `mcp__serena_get_symbols_overview` on the target file; for diagnostics before/after an edit use `mcp__serena_get_diagnostics_for_file`.
3. Confirm the **App vs Pages** boundary: if `app/` exists, you're here. `src/app/` is the same router (just under `src/`).
### 1. Place the RSC vs Client boundary correctly
- **Default = Server Component.** It can be `async`, can `await fetch()`/DB/`cookies()`/`headers()` directly, and streams over the network — never ships its source or heavy deps to the browser.
- Add `'use client'` **only** when a module needs `useState`/`useEffect`/event handlers/browser APIs/class components. A `'use client'` file CAN still import and render Server Components as `children` props (pass them down; never import an RSC into a client module).
- Rule of thumb confirmed across 8syncdev repos: **fetch data in the Server Component, pass serializable props to a small `'use client'` leaf** that owns only the interactivity. Keep the client surface minimal — it's what ends up in the bundle.
### 2. Data fetching + the fetch cache (the thing that silently serves stale data)
This is the #1 source of Next bugs. **Next 15+ changed the default: `fetch` is no longer cached.** `fetch(url)` behaves like `cache: 'no-store'` unless you opt in.
- **Dynamic / user-specific data** (CRM rows, auth, dashboards): leave uncached, or set `cache: 'no-store'` explicitly. The 8syncdev CRM api-client pins `cache: 'no-store'` for exactly this.
- **Slowly-changing / public data** (marketing, catalogue): `fetch(url, { next: { revalidate: 3600 } })` (time) or `{ next: { tags: ['products'] } })` then invalidate by tag.
- **Per-request data** (cookies/headers in the tree): that route is dynamic by definition — don't fight it.
- `unstable_cache(fn, keys, { revalidate, tags })` wraps a non-`fetch` function (DB calls) the same way.
- `generateStaticParams` for `[param]` pages you want prerendered at build.
When a page mysteriously shows stale data, the cause is almost always a `fetch` that picked up `force-cache` (or a leftover `{ revalidate }` you forgot). Grep the data path with `codegraph query "<fetch site>"` and read the option.
### 3. Mutations: Server Actions vs Route Handlers
| Need | Use |
|---|---|
| Form submit / button that mutates server state and revalidates | **Server Action** (`'use server'` async fn). Pass to a client component or a `<form action={fn}>`. Call `revalidatePath`/`revalidateTag` at the end. |
| Webhook receiver, third-party callback, streaming proxy, public REST endpoint | **Route Handler** (`app/api/<path>/route.ts`, export `GET`/`POST`/…). |
| Need raw `Request`/`Response` control (CORS, streaming body, proxy) | **Route Handler** — Server Actions can't do this. |
Server Actions are RPC over POST to a compiled endpoint; they revalidate the router cache automatically only if you ask. Route Handlers are plain HTTP.
### 4. Cache invalidation that actually fires
After a mutation, **explicitly** invalidate — nothing revalidates on its own:
- `revalidatePath('/dashboard')` (or `revalidatePath('/', 'layout')` for everything under a layout).
- `revalidateTag('products')` if you fetched with that tag.
- Both throw if called outside a Server Action / Route Handler / `generateMetadata` request context.
**Prove it** (see Acceptance): mutate, reload the page, assert the new value appears. A passing build does NOT prove revalidation.
### 5. Middleware (Edge runtime) — auth, rewrite, locale
`middleware.ts` at the project root (or `src/middleware.ts`) runs on the **Edge runtime** before routing, on (almost) every request.
- Use it for: auth redirects, locale prefix routing, A/B rewrite, geo redirect.
- It runs in Edge: **no** Node APIs, **no** most npm deps, **no** DB drivers. Do auth by verifying a cookie/JWT or calling a lightweight Edge-friendly endpoint.
- Scope it with a `matcher` config so it doesn't run on static assets / `/api` you don't own.
- next-intl v4 (used by `crm-pro-ai`) can do locale routing via the `[locale]` segment + `setRequestLocale` **without** a middleware, or with `createMiddleware` — match the repo's existing choice.
### 6. External backend client (Encore.ts / REST / GraphQL)
Two integration shapes, both present in 8syncdev repos — pick by reading the repo:
- **Generated typed client** (Encore.ts): `encore gen client <lang>` → `packages/core/src/api/client.ts`. `const client = new Client(Environment('prod'))` then `await client.user.getUsers({page:1,size:20})`. Fully typed; use in Server Components/Actions. (`8syncdev/8sync-verse:packages/core/src/api/client.ts`)
- **BFF proxy + shared api-client** (CORS-safe): a catch-all Route Handler forwards to the backend, injecting auth from an httpOnly cookie; Server Actions call the backend directly server-side. (`8syncdev/crm-pro-ai:apps/web/src/app/api/backend/[...slug]/route.ts` + `src/lib/actions/common/api-client.ts`)
Never put a raw backend URL + secret in a Client Component. Resolve env server-side; proxy browser calls through `/api`.
### 7. Metadata / SEO
- Static: `export const metadata: Metadata = {…}` in a layout/page (title template, openGraph, twitter, robots, metadataBase).
- Dynamic: `export async function generateMetadata({ params }): Promise<Metadata>` (params is a Promise in Next 15+ — `await` it).
- File conventions (zero code): `app/{icon,apple-icon,opengraph-image}.{tsx,png}`, `app/manifest.ts`, `app/robots.ts`, `app/sitemap.ts`. The 8syncdev repos use all of these.
### 8. Deployment: Vercel vs Docker/self-host vs static export; env vars
- **Vercel** (8syncdev default for web): multi-project monorepo — each `apps/<name>` is a Vercel project with its Root Directory; `turbo-ignore` skips unaffected apps; env vars per project (prefix `NEXT_PUBLIC_*` to expose to browser). Edge runtime + ISR/PPR + per-PR preview URLs.
- **Self-host / Docker**: set `output: 'standalone'` in `next.config.ts`, build, copy `.next/standalone` + `.next/static`, run `node server.js` (or `next start`). Resolve env at runtime, NOT build — `NEXT_PUBLIC_*` is inlined at build time.
- **Static export**: `output: 'export'` → fully static `out/` (no Server Actions, no middleware, no ISR). Used by `8sync-verse` learn app.
- **Env rule**: `NEXT_PUBLIC_*` is the *only* prefix the browser can see. Everything else is server-only. Verify with `mcp__codebase_memory_mcp_detect_changes` after wiring env so no secret leaks into a client module.
### 9. Build loop + UI verification
- **Plan the work as gated tasks**: `engine_plan` a slice whose verify commands are the project's real `next build` / `next lint` / `tsc --noEmit` / e2e. Pull the next task with `engine_next`, run `engine_verify` (ALL commands must pass), commit with `engine_advance`. Do NOT hand-roll loops.
- **UI = eyes, not assumption.** Start the dev server, then drive the real flow with the `browser` tool (`xd://browser`): `open` → `run` (`tab.observe()`, click, fill), screenshot the result. Capture artifacts with `8sync shot <url> -o /tmp/x.png`; compare before/after with `8sync diff-img`. A green `next build` does not prove the button works — a screenshot does.
- Fan out independent route edits with `task` subagents (scout for read-only route-tree mapping; worker for edits). Share the route-tree map via `local://` URIs, not pasted text.
## Acceptance check
Before declaring done, ALL of these must hold:
1. **`next build` clean** — zero errors; dynamic/static prerender report matches intent (routes you expect prerendered are prerendered, dynamic ones are dynamic).
2. **Route tree renders** — drive ≥1 real flow in the `browser`; screenshot evidence (`8sync shot`). No console errors on the pages you touched.
3. **Cache invalidation proven** — for every `revalidatePath`/`revalidateTag` the product depends on: mutate → reload → assert the new value is visible. State which path/tag and how you proved it.
4. **Boundary correct** — no `'use client'` leaks data-fetching that should be server-side; no Server Component reaches for `useState`. (Spot-check with `mcp__serena_get_diagnostics_for_file`.)
5. **Env hygiene** — no backend secret in a client module; `NEXT_PUBLIC_*` only where the browser truly needs it.
## Non-goals
- Pages Router (legacy) unless the repo actually uses it.
- Pages → App migration (separate task).
- Picking/imposing a CSS framework — follow the repo's existing Tailwind/CSS choice.
- Reinventing omp primitives — this skill composes codegraph/codebase-memory/serena/engine_*/browser/8sync verbs; it never reimplements them.