agentleFS
jayeshsojitra103jayeshsojitra103/claude-frontend-skills/skills/nextjs-app-router-architect/SKILL.md

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

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
  1. Distinctive:

    Use them for mutations, not as a general RPC layer.

    line 82
  2. Very specific:

    Use useOptimistic for perceived speed, with a rollback path on failure

    line 89
  3. Distinctive:

    Keep both routers only as long as the migration takes.

    line 105

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

LayerScopeInvalidate with
Request memoizationone render passautomatic
Data Cacheacross requests and deploysrevalidateTag, revalidatePath, revalidate
Full Route Cacherendered RSC payload, build/runtimerevalidatePath, redeploy
Router Cacheclient-side, per sessionrouter.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:

  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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.