nextjs-app-architecture — Claude Code skill: Next.js 16 app layout
A skill that keeps a Next.js 16 App Router codebase on one architecture: pages compose, features fetch. Useful when starting an app or auditing one.
npx skills add aurorascharff/nextjs-app-architecture-skill --skill nextjs-app-architecture -a claude-codeWhat nextjs-app-architecture does
The skill walks the agent through ordered steps: pick a mode (build from scratch or audit), place the work in a feature folder, write the query and the action, then build the component with its skeleton. Each step points at a reference file — feature folders, queries and actions, cache components, single-page applications. Framework mechanics stay in the Next.js docs; the skill links out instead of restating API signatures.
Eleven invariants define what every change must satisfy. Pages and layouts stay synchronous through params.then(), features receive an id or slug rather than raw params, queries sit in <domain>-queries.ts with import 'server-only', actions in <domain>-actions.ts with 'use server'. The page owns the <Suspense> boundary and the skeleton is exported from the same file as the component.
One setup step comes first: the project has to let the agent read version-matched Next.js docs, via AGENTS.md or bundled docs, and the skill tells you to create or refresh them when they are missing. Skip that and the agent works from memory, which is a problem because Next.js 16 APIs differ from earlier versions. Some links point to preview.nextjs.org, so they cover canary-only APIs.
What it helps with
- split App Router pages by feature instead of by route
- turn a route-loader page into an async server component
- decide where queries, actions and components belong
- place Suspense boundaries without losing the static shell
- export skeletons so the layout stops shifting on load
Example prompts for the agent
- audit app/ and list every page that fetches on its own
- add a favorites feature following this architecture
- turn on Cache Components and tag the feed cache
How to install nextjs-app-architecture
1Skills CLI (Vercel)
npx skills add aurorascharff/nextjs-app-architecture-skill --skill nextjs-app-architecture -a claude-code2Manually: put the files in place
~/.claude/skills/nextjs-app-architecture/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/nextjs-app-architecture/
1Skills CLI (Vercel)
npx skills add aurorascharff/nextjs-app-architecture-skill --skill nextjs-app-architecture -a codex2Manually: put the files in place
.agents/skills/nextjs-app-architecture/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add aurorascharff/nextjs-app-architecture-skill --skill nextjs-app-architecture -a cursor2Manually: put the files in place
.cursor/skills/nextjs-app-architecture/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install aurorascharff/nextjs-app-architecture-skill nextjs-app-architecture2Manually: put the files in place
.github/skills/nextjs-app-architecture/SKILL.md1Skills CLI (Vercel)
npx skills add aurorascharff/nextjs-app-architecture-skill --skill nextjs-app-architecture -a gemini-cli2Platform CLI
gemini skills install https://github.com/aurorascharff/nextjs-app-architecture-skillManually: put the files in place
.devin/skills/nextjs-app-architecture/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add aurorascharff/nextjs-app-architecture-skill --skill nextjs-app-architecture -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about nextjs-app-architecture
How do I install nextjs-app-architecture in Claude?
Run npx skills add aurorascharff/nextjs-app-architecture-skill --skill nextjs-app-architecture -a claude-code. The skill lands at ~/.claude/skills/nextjs-app-architecture/SKILL.md.
What does nextjs-app-architecture do?
It sets the order of work on a Next.js 16 App Router app: where features go, how queries and actions are written, where Suspense and skeletons belong. It covers both new builds and reviews of existing code.
What has to be ready before using the skill?
The project needs to be set up so the agent can read docs matching its Next.js version, through AGENTS.md or bundled docs. The skill tells you to create or refresh them if they are missing.
Source text
SKILL.md · MIT
---
name: nextjs-app-architecture
description: Build or audit Next.js 16 App Router apps using a next-beats-style React Server Components architecture. Use when scaffolding a new app, adding a feature, reviewing an existing app, refactoring route-loader-shaped pages into feature-owned async server components, deciding where queries/actions/components live, keeping pages synchronous with `params.then()`, placing Suspense boundaries, choosing the client/server boundary, designing skeletons, preventing CLS, or enabling Cache Components. Also use when the user asks about RSC composition, components receiving IDs instead of route params, `'use cache'`, `cacheTag`, `updateTag`, static-shell prerendering, or making an app easier for AI agents to modify.
license: MIT
metadata:
author: aurorascharff
version: "1.4.1"
---
# Next.js App Architecture
A workflow for building and auditing Next.js 16+ App Router apps so they follow one consistent, feature-sliced RSC architecture like `next-beats`.
**Follow the workflow below step by step** — it produces the invariants by construction. Load the reference a step names for the decision it depends on. Get framework _mechanics_ (API signatures, config options, hook contracts) from the linked docs — don't restate or improvise them. Links for canary-only Next.js APIs use `preview.nextjs.org`.
## Prerequisite
Before changing a Next.js app, make sure the project is set up for AI agents to read version-matched docs. Follow the [AI Coding Agents guide](https://preview.nextjs.org/docs/app/guides/ai-agents): prefer the project's `AGENTS.md` / bundled docs, and create or refresh them when missing. Then use this skill for architecture decisions.
## Architecture target
Build pages that describe the loading experience, not pages that act like route loaders:
- `app/**/page.tsx` and `layout.tsx` are synchronous composition surfaces: static chrome, section headings, `<Suspense>` boundaries, error boundaries, and transition wrappers.
- Feature components own their reads on the server. They receive minimal stable inputs (`id`, `slug`, `handle`, parsed filter values) or already-fetched records, never raw `params` / `searchParams`.
- Queries and actions live in the feature folder. Components import queries; client leaves import actions directly.
- When server tags and client query keys describe the same feature data, a pure feature-local cache contract owns those identities.
- Stable chrome, wrappers, and skeletons preserve layout: cards/panels stay outside Suspense, and fallbacks swap only the data-dependent body.
## Invariants (what every change must satisfy)
The non-negotiables. The workflow produces them; the final check verifies them.
1. **Pages compose, they never fetch.** A page/layout imports feature components and places `<Suspense>`. No queries or domain logic inline. Tiny route-local control-flow helpers are allowed only when they exist to place a boundary around `connection()`, `redirect()`, or a resolved route prop.
2. **Pages stay synchronous.** Use `params.then()` / `searchParams.then()` or `Promise.all([params, searchParams]).then(...)`, never `await params` at the top — so chrome paints into the static shell and only data-dependent sections suspend.
3. **Feature components receive IDs, not route props.** Resolve `params` / `searchParams` at the page boundary and pass plain values (`id`, `slug`, `query`) into features.
4. **Async server component is the default.** `'use client'` only for hooks, event handlers, or browser APIs — and only on leaves, never on parents of server content.
5. **The page owns the Suspense boundary; the feature owns the skeleton.** Features never pre-wrap themselves in `<Suspense>`; stable wrappers/cards/chrome wrap the boundary instead of being duplicated in fallback and final content.
6. **Skeletons live in the same file as the component**, exported alongside it, defined at the end. `Feed` and `FeedSkeleton` are siblings.
7. **Queries live in `<domain>-queries.ts`** (`import 'server-only'`); **actions live in `<domain>-actions.ts`** (`'use server'`). The file name matches the folder, even for sub-concepts.
8. **Feature folders follow product ownership.** Entity-owned sub-concepts (favorite, like, vote, bookmark) fold into their parent. A route-level experience that composes multiple domains may own its UI and state in a separate feature while each domain keeps its queries and actions.
9. **Client components import actions directly** — never receive a server action as a prop just to call it.
10. **Feature-local cache coordination stays with its domain.** Put pure tags/keys in `<domain>-cache.ts`, client query definitions in `<domain>-query-options.ts`, hook wrappers in `hooks/use-*.ts`, and tiny client leaves in `components/`; promote support code only after real cross-feature reuse.
11. **Interactive async UI keeps server data on the server and client state local to the interaction.** Use `useOptimistic`, `useTransition`, reducers, URL/search params, and form actions instead of mirrored prop state, derived-state effects, or hand-rolled pending arrays.
## Workflow
Run these in order for build-from-scratch, feature work, or audits. Each step names the reference to consult and the check it must pass.
1. **Choose mode.**
- **Build from scratch:** sketch routes, real domain nouns, static shell, and expected loading groups before writing code.
- **Audit/refactor:** scan current `app/` pages first; list every async page, page-level query import, route prop leak, missing Suspense boundary, and feature folder mismatch.
→ `references/example.md` for the target shape; `references/feature-folders.md` for placement.
✓ You know whether you are creating the architecture or converting loader-shaped code into it.
2. **Place the work.** Decide the feature folder before writing anything.
→ `references/feature-folders.md` (decision tree + merge rules).
✓ A real domain, a cross-domain product experience, or folded into the right parent.
3. **Write the query and, when a client cache shares its data, the cache contract.** Put server reads in `<domain>-queries.ts` with `import 'server-only'`; keep shared tag/key identities in a pure `<domain>-cache.ts`.
→ `references/queries-actions.md`; for SWR/TanStack Query → `references/single-page-applications.md`; with `cacheComponents: true`, also → `references/cache-components.md`.
✓ Cache identities are defined once; server reads are server-only, cached/tagged/lifetimed under Cache Components, and return domain types rather than ORM rows.
4. **Write the action** (if there's a mutation). `features/<domain>/<domain>-actions.ts`, `'use server'` at the top.
→ `references/queries-actions.md`.
✓ Re-checks auth, validates input, invalidates matching cache tags under Cache Components (`refresh()` only for justified dynamic reads), returns a discriminated union.
5. **Build the component + skeleton.** `features/<domain>/components/<name>.tsx`: an async server component that awaits its own query from minimal props; `'use client'` only on interactive leaves.
→ `references/components.md`; for a client data library or strict-SPA/CSR feature → `references/single-page-applications.md`.
✓ Component receives IDs/handles/parsed filters or already-resolved records, not `params`; skeleton is a sibling export at the end; no alias skeleton wrappers.
6. **Compose the page.** `app/<route>/page.tsx`: synchronous, `params.then()` / `Promise.all(...).then(...)`, place Suspense around data bodies, and wrap the section that can fail independently in an error boundary.
→ `references/pages-suspense.md`; with `cacheComponents: true`, also → `references/instant-navigation.md` for which links prefetch and which reads wait for the navigation.
✓ Route props become plain values; stable cards/sections wrap Suspense when they set layout; boundaries stay visible at the page.
7. **Add interaction** (if any): optimistic updates, pending state, toasts, confirmation.
→ `references/ux-patterns.md`.
✓ Feedback isn't doubled; optimistic reducers/actions live with the feature; URL/search params own shareable state; client effects synchronize external systems, not derived React state.
8. **Verify** against the checklist below before declaring done. When the change touches a loading sequence, lock it in with an end-to-end test.
→ `references/testing.md`.
✓ The test asserts which content is available before uncached work resolves, using visibility-aware locators.
## Verify before done
Inspect the diff against every invariant — each is checkable by reading the changed files:
- [ ] No page/layout imports a `*-queries` file or defines reusable domain UI inline; any inline helper is route-local control flow only.
- [ ] Every page with params is synchronous and uses `params.then()` / `searchParams.then()` / `Promise.all([params, searchParams]).then(...)`.
- [ ] Feature components receive plain IDs/handles/parsed filters or resolved records; no feature prop is named `params` or `searchParams`.
- [ ] Every `<Suspense>` for page data sits in the page; no feature pre-wraps itself.
- [ ] Stable wrappers/cards/chrome sit outside Suspense; fallback and final content do not duplicate the same outer card.
- [ ] Every component has its real `*Skeleton` in the same file, at the end; no tiny skeleton aliases just to pass props.
- [ ] Skeleton and content are the same height (measured), section headings sit outside the boundary, and sections that can be empty reserve their space.
- [ ] Sections below a variable-height section are nested sibling boundaries inside it, not awaited by it and not passed through it as children.
- [ ] `unstable_prefetch()` / `unstable_navigation()` are awaited in the component before its query call; no wrapper query exists just to hold the gate.
- [ ] Every `*-queries.ts` starts with `import 'server-only'`; every `*-actions.ts` with `'use server'`.
- [ ] With `cacheComponents: true`, reusable reads use `'use cache'` / `cacheTag` / `cacheLife`, or `'use cache: private'` / `'use cache: remote'` when appropriate; any dynamic read is intentional and justified.
- [ ] A personalized shared-cache scope receives trusted, normalized identity values from an uncached feature wrapper; it never reads request APIs or trusts a client-provided identity inside the cache scope.
- [ ] Reads whose every write calls `updateTag()` use `cacheLife('max')`; a shorter profile is reserved for data that changes outside the app's actions.
- [ ] Mutations touching cached reads call `updateTag()` / `revalidateTag(..., 'max')` for the matching tags; `refresh()` is not a substitute for tag invalidation.
- [ ] `prefetch={true}` is reserved for links users predictably follow, or for rows whose per-link prefetch was made cheap by gating the heavy part behind `unstable_navigation()`.
- [ ] Action files are named `<folder>-actions.ts`; no entity-owned sub-concept spawned its own folder, and cross-domain product features do not take ownership of entity queries/actions.
- [ ] Features with both server tags and client query keys define them once in a pure `<domain>-cache.ts`; queries, actions, hydration, query options, and hooks import from it.
- [ ] Feature-local client-support files sit in the smallest fitting place: query options at the feature root, `use-*` hook wrappers in `hooks/`, leaf components in `components/`, and shared support only after real cross-feature reuse.
- [ ] `'use client'` components are leaves — they import actions/hooks/providers, not async server components. A client component that needs server data has a server half in the feature (`ComposePanel` → `ComposePanelClient`); no page or layout creates query promises for it.
- [ ] Client leaves use `useOptimistic`, transitions, reducers, URL state, or form actions for interaction; they do not call `setState` in effects for derived React state.
- [ ] Forms whose action can reject input keep what the user typed: the action state returns the submitted values and the fields read them as `defaultValue`.
- [ ] Mutations validate their input and invalidate the affected data.
## Reference index
- **`references/feature-folders.md`** — where code goes: folder layout, cache contracts, naming, and merging sub-concepts.
- **`references/queries-actions.md`** — query/action rules: server-only, dedup, validation, invalidation, return shape.
- **`references/components.md`** — server/client boundary, skeletons, `use()`, single-use helpers, live data.
- **`references/pages-suspense.md`** — page composition, `params.then()`, Suspense placement, CLS, error boundaries.
- **`references/instant-navigation.md`** — prefetching decisions: `prefetch={true}`, the three render stages and their gates, validating what a navigation shows, hidden routes.
- **`references/cache-components.md`** — the `cacheComponents` decisions: which reads to cache, which directive to use, how to invalidate.
- **`references/single-page-applications.md`** — client cache decisions: placement, server seeding, Cache Components coordination, hydration, and mutations.
- **`references/ux-patterns.md`** — interaction decisions: optimistic vs pending vs inline error, toasts, action-prop, confirmations.
- **`references/testing.md`** — end-to-end tests: asserting the loading sequence with `instant()`, locating content across hidden routes, testing mutations.
- **`references/example.md`** — the next-beats reference app: invariant → file map, for seeing any rule in real code.
© aurorascharff · MIT · aurorascharff/nextjs-app-architecture-skill