nextjs-app-router-architect
Skill0 starsChanged 3 months agoMITView on GitHub
Design, review, and migrate Next.js App Router applications — Server versus Client Component boundaries, the four-layer caching model, Server Actions, streaming with Suspense, and Pages Router migration. Use this skill whenever the user mentions the Next.js App Router, React Server Components, "use client" or "use server", generateMetadata, revalidatePath or revalidateTag, ISR, route handlers, hydration errors, stale data after mutations, or is deciding how to structure a new Next.js application — including vaguer prompts like "my Next app fetches too much on the client" or "why is my data stale".
In this repository5 other files it gives its agents.
- ai-ui-patternsSkill
- component-test-writerSkill
- frontend-a11y-auditSkill
- microfrontend-architectSkill
- react-performance-auditSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 82
Use them for mutations, not as a general RPC layer.
- Very specific: line 89
Use useOptimistic for perceived speed, with a rollback path on failure
- Distinctive: line 105
Keep both routers only as long as the migration takes.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Next.js App Router Architect
The App Router's two genuinely hard parts are the server/client boundary and caching. Almost every real bug traces back to one of them, so diagnose in that order.
The boundary rule
Server Components are the default and should stay the default. Add "use client" at the
deepest point that actually needs interactivity, because the directive marks a boundary,
not a file: everything imported below it joins the client bundle.
The mistake that costs the most bundle weight is putting "use client" at the top of a
page to make one button interactive. Extract the button instead and leave the page on the
server.
Server Components can render Client Components. Client Components cannot import Server
Components — but they can receive them as children or props, which is the escape hatch
for layout shells, providers, and theme wrappers:
// app/layout.tsx — stays a Server Component
<ClientProviders>{children}</ClientProviders>Props crossing the boundary must be serializable. Functions, class instances, Dates in some versions, and Symbols will fail — pass ids and re-fetch, or pass a Server Action.
Reach for "use client" only for: event handlers, hooks holding state or effects, browser
APIs, and third-party components that use any of the above.
The caching model
Four independent layers, and confusing them produces the two classic symptoms — data that will not update, and data that will not stay cached:
| Layer | Scope | Invalidate with |
| Request memoization | one render pass | automatic |
| Data Cache | across requests and deploys | revalidateTag, revalidatePath, revalidate |
| Full Route Cache | rendered RSC payload, build/runtime | revalidatePath, redeploy |
| Router Cache | client-side, per session | router.refresh(), Server Action revalidation |
Defaults shifted between Next 14 and 15 — fetch is no longer cached by default in 15, and
route handlers are dynamic unless configured. Confirm the version in package.json before
diagnosing anything cache-related, since the same code behaves differently across the two.
Tag reads, invalidate by tag on write. It scales better than path invalidation because one mutation rarely maps to one URL:
const product = await fetch(url, { next: { tags: [`product:${id}`] } });
// in the Server Action that mutates it:
revalidateTag(`product:${id}`);"Stale after mutation" is almost always a missing revalidateTag/revalidatePath in the
action, or a client cache that needs router.refresh(). Read references/caching.md
before proposing a fix — guessing here wastes more time than reading.
Streaming and layout
Fetch in parallel by default. Sequential awaits in one component create a waterfall:
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);Wrap slow independent regions in <Suspense> so the shell paints immediately. loading.tsx
gives a route-level boundary for free; per-section boundaries are usually better because
they let fast content render while one slow query resolves.
Give every skeleton the same dimensions as its content, or streaming trades an LCP win for a CLS regression.
Server Actions
Use them for mutations, not as a general RPC layer. Non-negotiables, because an action is a public HTTP endpoint whatever it looks like in the source:
- Authenticate and authorize inside the action — never rely on the calling UI
- Validate input with a schema (Zod or equivalent); the client can send anything
- Return typed errors rather than throwing raw ones into the client
- Revalidate the affected tags before returning
- Use
useOptimisticfor perceived speed, with a rollback path on failure
Migration from the Pages Router
Both routers run side by side, so migrate incrementally rather than in one branch:
- Move
_app/_documentconcerns intoapp/layout.tsx; providers become a client shell - Port leaf routes first — lowest traffic and fewest dependencies
- Replace
getServerSidePropswith directawaitin the component;getStaticPropswithgenerateStaticParamsplusrevalidate - Swap
next/routerfornext/navigation— the APIs differ (useSearchParamsis read-only and suspends, so pages using it need a Suspense boundary or they opt into dynamic rendering) - Replace
next/headwith themetadataexport orgenerateMetadata - Move API routes to route handlers, or delete them where a Server Action fits better
Keep both routers only as long as the migration takes. A permanent split doubles the mental model for everyone on the team.
Review checklist
Work top to bottom and report severity + location + fix:
"use client"higher in the tree than necessary — measure the bundle cost- Secrets or server-only modules imported into a client subtree (add
server-only) - Sequential awaits that could be
Promise.all - Mutations with no
revalidateTag/revalidatePath - Server Actions missing auth or input validation
useEffectfetching data a Server Component could fetch- Missing
<Suspense>around slow regions; missing skeleton dimensions - Hydration mismatches from
Date.now(),Math.random(), orwindowduring render generateMetadatamissing on indexable routes- Unbounded
dynamic = 'force-dynamic'used to paper over a caching misunderstanding
Output format
Design tasks: route tree with server/client annotations → data fetching and caching plan → streaming boundaries → mutation flow. Reviews: findings table ordered by severity, each with the mechanism explained, then a migration sequence when relevant.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

