# Nextjs App Router Architect

> 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".

- Skill: `jayeshsojitra103/nextjs-app-router-architect` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add jayeshsojitra103/nextjs-app-router-architect`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jayeshsojitra103/nextjs-app-router-architect/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: jayeshsojitra103 (https://skillmd.com/u/jayeshsojitra103)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jayeshsojitra103/nextjs-app-router-architect

---


# 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:

```tsx
// 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:

```ts
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 `await`s in one component create a waterfall:

```tsx
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:

1. Authenticate and authorize **inside** the action — never rely on the calling UI
2. Validate input with a schema (Zod or equivalent); the client can send anything
3. Return typed errors rather than throwing raw ones into the client
4. Revalidate the affected tags before returning
5. Use `useOptimistic` for 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:

1. Move `_app`/`_document` concerns into `app/layout.tsx`; providers become a client shell
2. Port leaf routes first — lowest traffic and fewest dependencies
3. Replace `getServerSideProps` with direct `await` in the component;
   `getStaticProps` with `generateStaticParams` plus `revalidate`
4. Swap `next/router` for `next/navigation` — the APIs differ (`useSearchParams` is
   read-only and suspends, so pages using it need a Suspense boundary or they opt into
   dynamic rendering)
5. Replace `next/head` with the `metadata` export or `generateMetadata`
6. 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:

1. `"use client"` higher in the tree than necessary — measure the bundle cost
2. Secrets or server-only modules imported into a client subtree (add `server-only`)
3. Sequential awaits that could be `Promise.all`
4. Mutations with no `revalidateTag`/`revalidatePath`
5. Server Actions missing auth or input validation
6. `useEffect` fetching data a Server Component could fetch
7. Missing `<Suspense>` around slow regions; missing skeleton dimensions
8. Hydration mismatches from `Date.now()`, `Math.random()`, or `window` during render
9. `generateMetadata` missing on indexable routes
10. 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.

