Next.js Feature Architecture
Keep an App Router feature explainable as:
request -> route contract -> page composition -> feature -> data access
Apply this as an ownership model, not a directory template. Follow the target
repository and its installed Next.js version. Verify current documentation
before choosing version-sensitive request, rendering, mutation, or caching
APIs.
Set the architecture scope
Judge the existing architecture on its merits. Do not preserve a boundary
merely because it already exists, and do not impose this model unless the
task authorizes architectural change.
- When implementation is limited to a feature or fix, keep the app working,
extract one capability from the existing route, and leave neighboring
routes alone. Surface leftover old boundaries as debt with a repayment
trigger instead of silently restructuring them.
- When the task includes an authorized redesign or migration, define a
coherent target from these principles and move toward it in
behavior-preserving steps. Do not rewrite the application to match a
reference tree.
- Invoking this skill selects an architectural lens; it does not expand the
requested scope.
Read conditional guidance
- When a feature grows beyond one composition root or gains a named external
compositor, read
references/example-structure.md. Adapt
it; never reproduce trees mechanically.
- When several widgets coordinate URL, server, query-cache, local, persisted,
optimistic, or shared transient state, read
references/state-coordination.md.
- When the repository uses shadcn and Tailwind and shared UI boundaries affect
the task, read references/shadcn.md.
Read only the references that apply. Load at most one matching scenario.
Preserve six invariants
- Routes own the request contract.
- Pages compose features.
- Features own behavior.
- Every state value has one authoritative owner.
- Server Components are the default.
- Dependencies cross explicit, environment-safe boundaries.
When the product has a shared visual language, Shared UI owns that language.
Add structure only when it protects one of these boundaries.
Use ownership vocabulary
- A feature owns application behavior or a user capability.
- A widget is an independently composable UI region exposed by a feature,
with its own data requirements and UX lifecycle. It is not another
architecture layer: a feature may expose several widgets, and a widget does
not require its own directory.
- Shared UI owns reusable visual and interaction primitives without feature
behavior or authoritative product state.
- A shared product component composes shared UI for several features but
owns neither application behavior nor authoritative state.
- A domain or platform module owns reusable headless policy, data access, or
infrastructure capability below feature use cases. It exposes narrow
contracts and does not depend on route-facing features.
Use this heuristic:
Application behavior or user capability? -> feature
Independently composable feature region? -> widget
Reusable visual or interaction primitive? -> shared UI
Reusable product composition, no natural feature owner? -> shared product component
Reusable headless policy or capability? -> domain or platform module
Feature-specific behavior or composition? -> keep it in that feature
Choose feature boundaries by the knowledge and changes they contain, not by
screen rectangles. Keep behavior together when policy, state, failure, and
lifecycle change together. Split when product policy, authorization, freshness,
or consumers evolve independently and the resulting interface hides meaningful
complexity. Treat team or deployment boundaries as additional evidence, not the
sole reason for a feature.
Run the decision loop
- Which route owns the request contract?
- Which feature owns the behavior?
- Which independently composable regions should that feature expose as
widgets?
- Who is the authoritative owner of each state value?
- Can this stay on the server?
- What can render, fail, or refresh independently?
- Which feature operation owns the use case, and which lower module owns any
reused policy or data access?
- What is the cache and freshness contract?
- Do dependencies cross only public, runtime-safe boundaries?
- Is the feature inventing visual or interaction behavior that belongs to
shared UI?
Establish ownership
- Treat
params, searchParams, cookies, headers, locale, and other request
inputs as boundary data. The route owns their contract semantically but need
not resolve every value eagerly at the top of the tree.
- Resolve request-time values at the narrowest boundary that needs them. Parse,
validate, normalize, and default each value there before feature behavior
uses it. Do not await
params or searchParams at the page top when that
would block a static shell or make a Suspense boundary theatrical.
- Verify identity and authorization on the server; never trust client-provided
claims merely because route inputs were parsed.
- Pass normalized request values into cached or otherwise reusable work.
- Keep layouts focused on the document or segment-wide shell, shared providers,
navigation, and presentation that truly persists across child routes. Do not
move one page's widget composition or feature behavior into a layout.
- Keep pages as route composition roots: parse the route contract, compose
public feature surfaces, own route-level layout, and place loading and error
boundaries. Prefer one feature root for a page that presents one capability;
let that root hide its widget decomposition, data access, and internal layout.
Export an individual widget only when a named external compositor needs it,
such as a page interleaving independent features, a layout or parallel route,
or another route that consumes a subset.
- Keep feature UI, contracts, operations, and actions together. Create
subdirectories only as responsibilities emerge.
- Keep feature-specific components, hooks, operations, and utilities inside
their owner; do not turn global technical folders into dumping grounds.
- Give a workflow that genuinely spans multiple features an explicit
orchestration owner. The orchestrator depends on public participant
operations or injected ports; participants do not import one another. Pass a
workflow command into participant UI when it must emit an intent, rather than
creating a reverse import. Do not hide cross-feature behavior in a participant
or generic shared folder.
- Move product-aware code to a shared product component only when it has
multiple real consumers and no feature is its natural owner.
- Let a feature operation own its use-case contract and orchestration. Reuse
central authorization, domain policy, data access, telemetry, and vendor
adapters through headless domain or platform modules when those concerns span
features. Recheck authorization at every trusted entry point; centralizing
policy does not make callers trusted.
Protect module and runtime boundaries
Prefer UI dependencies that flow from app to features to shared product
components to shared UI. Let server dependencies flow from feature operations
through domain or platform contracts to infrastructure. These are ownership
directions, not required folders. Hide storage, transport, credentials, and
vendor details behind the narrowest meaningful contract.
- Expose intentional feature entry points; reject deep imports into another
feature's internals.
- Compose peer features in
app or an explicit workflow instead of importing
one feature's UI or internals into another. When several features reuse a
headless capability, move that capability below them rather than choosing one
feature as the accidental owner.
- Keep server-only, client-only, and environment-neutral exports distinct. Do
not re-export them through one ambiguous barrel.
- Use entry points such as
feature, feature/server, and feature/client
only when real consumers need those different runtime capabilities. Do not
add symmetric barrels by convention.
- Mark sensitive modules with
server-only and browser-bound modules with
client-only when that makes invalid imports fail early.
- Give browser reads a browser-safe transport. A client query must not import a
server-only feature operation; let a Route Handler or the repository's
established client transport delegate to that operation.
- Pass only the data a Client Component needs across the server/client
boundary, using an explicit serializable DTO or view model rather than raw
storage or vendor objects.
- Prefer route-level composition when two features only need to appear or react
to the same route state together.
- When the repository's scale makes boundary drift costly, enforce public
entry points and forbidden import directions with its package, lint, or
dependency checks. Do not introduce enforcement tooling merely because this
skill was invoked.
Assign state deliberately
Choose one authoritative owner for every state value:
| State |
Typical owner |
| Navigable or shareable view state |
URL |
| Transient interaction state |
Local Client Component |
| Persistent UI preference |
Cookie or browser storage |
| Authoritative entity data |
Server or data layer |
| Temporary optimistic projection |
Client with explicit reconciliation |
| Continuously refreshed browser data |
Client cache, when justified |
- Put state in the URL when opening a copied URL should restore the view.
- Treat URL changes as navigation. Prefer links or forms where they fit, and
centralize parameter semantics and dependent resets such as pagination.
- Do not introduce Context, a global store, or a client query cache merely to
avoid deciding ownership.
- Allow derived or optimistic copies only when their source and reconciliation
behavior are explicit.
Compose from the server
- Start with Server Components. Introduce a Client Component only for browser
APIs, effects, local interaction, event handlers, high-frequency updates, or
optimistic feedback.
- Keep
"use client" boundaries close to the interaction because every import
below that boundary enters the client module graph.
- Let a server-rendered widget own what data it needs; let a feature operation
own how that data is obtained.
- Start independent work independently. Keep sequential work only when one
result truly depends on another.
- Use Server Functions or Actions for mutations initiated by the application.
Use Route Handlers when an HTTP boundary is itself required. Do not use
Server Actions as general read APIs.
- Validate and authorize inside every trusted mutation boundary, then make
invalidation or refresh behavior explicit.
- Prefer links and forms for navigation and form-like interactions when they
provide a useful baseline; add client behavior for material UX improvements.
Design loading, failure, and caching
- Place Suspense around regions that can meaningfully load, stream, reveal, or
refresh independently. Use route-level loading files when the whole segment
shares that lifecycle.
- Keep a widget-shaped skeleton or fallback with the feature that owns the
widget. Let the page place or compose that fallback; do not duplicate feature
geometry in a route-level loading file unless the whole segment deliberately
owns one coordinated shell.
- Match each fallback to the visual shape it replaces. Reset a boundary with a
key only when the content identity truly changed.
- Model empty results, validation failures, denied access, rejected mutations,
and known dependency failures as expected outcomes. Use
notFound(),
redirect(), and the repository's forbidden/unauthorized helpers at those
boundaries.
- Reserve error boundaries and route error handling for unexpected failures.
Define loading, empty, expected-failure, and unexpected-failure behavior
before declaring the feature complete.
- Treat caching as part of each server operation's data contract, not as an
incidental optimization. Keep the policy near the operation and define what
is cached, what identifies the cache entry, how long it may be stale, what
invalidates it, who owns that invalidation, and how users or tenants remain
isolated.
- Treat a server-render cache and a browser query cache as separate
representations.
queryClient.invalidateQueries cannot refresh a Server
Component.
- Detect whether the repository uses Cache Components. Separate invalidating
cached data from making the current UI observe it. With Cache Components, use
updateTag in a Server Action for immediate read-your-own-writes,
revalidateTag(tag, "max") for stale-while-revalidate, and revalidatePath
when the path is the intended invalidation scope. refresh() refreshes the
client router from a Server Action but does not invalidate cached data.
Without Cache Components, fetch is uncached by default; use the repo's
unstable_cache / revalidateTag / revalidatePath primitives. Never
assume historical fetch cache defaults still apply.
- Cookie
.set / .delete are illegal during RSC render. Persist preferences
only in a Server Function or Route Handler.
Verify the result
Implement the smallest complete slice. Test pure route parsing and feature
operations directly; test navigation, rendering, mutation, and cache wiring at
their integration boundaries.
Confirm that:
- external inputs become typed state before reaching feature logic;
- pages compose rather than implement features;
- every behavior and state value has a clear authoritative owner;
- Server Components remain the default and client boundaries stay narrow;
- server-only code cannot enter the client graph;
- consumers use small, earned public feature APIs rather than internals;
- features compose shared UI rather than introducing competing primitives;
- shared product components exist only with multiple real consumers and no
natural feature owner;
- infrastructure details remain below feature operations;
- reused domain and platform policy has a clear lower owner and does not create
peer-feature dependencies;
- independent work is not accidentally serialized;
- loading and failure boundaries match meaningful user experiences;
- mutations validate, authorize, and invalidate deliberately;
- caching has explicit identity, freshness, invalidation ownership, and
isolation semantics, and UI refresh is not mistaken for invalidation;
- abstractions and shared code represent real boundaries rather than ceremony.
Correct unclear ownership or dependency direction before adding another layer.
1---2name: nextjs-feature-architecture3description: Design, implement, refactor, or review Next.js App Router architectures using feature-oriented server composition. Use when deciding route or page responsibilities, feature and UI boundaries, Server and Client Components, state ownership, data access, Suspense, errors, mutations, caching, or dependency direction. Use for redesigns or migrations only when the task authorizes architectural change.4---56# Next.js Feature Architecture78Keep an App Router feature explainable as:910```text11request -> route contract -> page composition -> feature -> data access12```1314Apply this as an ownership model, not a directory template. Follow the target15repository and its installed Next.js version. Verify current documentation16before choosing version-sensitive request, rendering, mutation, or caching17APIs.1819## Set the architecture scope2021Judge the existing architecture on its merits. Do not preserve a boundary22merely because it already exists, and do not impose this model unless the23task authorizes architectural change.2425- When implementation is limited to a feature or fix, keep the app working,26 extract one capability from the existing route, and leave neighboring27 routes alone. Surface leftover old boundaries as debt with a repayment28 trigger instead of silently restructuring them.29- When the task includes an authorized redesign or migration, define a30 coherent target from these principles and move toward it in31 behavior-preserving steps. Do not rewrite the application to match a32 reference tree.33- Invoking this skill selects an architectural lens; it does not expand the34 requested scope.3536## Read conditional guidance3738- When a feature grows beyond one composition root or gains a named external39 compositor, read40 [references/example-structure.md](references/example-structure.md). Adapt41 it; never reproduce trees mechanically.42- When several widgets coordinate URL, server, query-cache, local, persisted,43 optimistic, or shared transient state, read44 [references/state-coordination.md](references/state-coordination.md).45- When the repository uses shadcn and Tailwind and shared UI boundaries affect46 the task, read [references/shadcn.md](references/shadcn.md).4748Read only the references that apply. Load at most one matching scenario.4950## Preserve six invariants51521. Routes own the request contract.532. Pages compose features.543. Features own behavior.554. Every state value has one authoritative owner.565. Server Components are the default.576. Dependencies cross explicit, environment-safe boundaries.5859When the product has a shared visual language, Shared UI owns that language.60Add structure only when it protects one of these boundaries.6162## Use ownership vocabulary6364- A **feature** owns application behavior or a user capability.65- A **widget** is an independently composable UI region exposed by a feature,66 with its own data requirements and UX lifecycle. It is not another67 architecture layer: a feature may expose several widgets, and a widget does68 not require its own directory.69- **Shared UI** owns reusable visual and interaction primitives without feature70 behavior or authoritative product state.71- A **shared product component** composes shared UI for several features but72 owns neither application behavior nor authoritative state.73- A **domain or platform module** owns reusable headless policy, data access, or74 infrastructure capability below feature use cases. It exposes narrow75 contracts and does not depend on route-facing features.7677Use this heuristic:7879```text80Application behavior or user capability? -> feature81Independently composable feature region? -> widget82Reusable visual or interaction primitive? -> shared UI83Reusable product composition, no natural feature owner? -> shared product component84Reusable headless policy or capability? -> domain or platform module85Feature-specific behavior or composition? -> keep it in that feature86```8788Choose feature boundaries by the knowledge and changes they contain, not by89screen rectangles. Keep behavior together when policy, state, failure, and90lifecycle change together. Split when product policy, authorization, freshness,91or consumers evolve independently and the resulting interface hides meaningful92complexity. Treat team or deployment boundaries as additional evidence, not the93sole reason for a feature.9495## Run the decision loop96971. Which route owns the request contract?982. Which feature owns the behavior?993. Which independently composable regions should that feature expose as100 widgets?1014. Who is the authoritative owner of each state value?1025. Can this stay on the server?1036. What can render, fail, or refresh independently?1047. Which feature operation owns the use case, and which lower module owns any105 reused policy or data access?1068. What is the cache and freshness contract?1079. Do dependencies cross only public, runtime-safe boundaries?10810. Is the feature inventing visual or interaction behavior that belongs to109 shared UI?110111## Establish ownership112113- Treat `params`, `searchParams`, cookies, headers, locale, and other request114 inputs as boundary data. The route owns their contract semantically but need115 not resolve every value eagerly at the top of the tree.116- Resolve request-time values at the narrowest boundary that needs them. Parse,117 validate, normalize, and default each value there before feature behavior118 uses it. Do not await `params` or `searchParams` at the page top when that119 would block a static shell or make a Suspense boundary theatrical.120- Verify identity and authorization on the server; never trust client-provided121 claims merely because route inputs were parsed.122- Pass normalized request values into cached or otherwise reusable work.123- Keep layouts focused on the document or segment-wide shell, shared providers,124 navigation, and presentation that truly persists across child routes. Do not125 move one page's widget composition or feature behavior into a layout.126- Keep pages as route composition roots: parse the route contract, compose127 public feature surfaces, own route-level layout, and place loading and error128 boundaries. Prefer one feature root for a page that presents one capability;129 let that root hide its widget decomposition, data access, and internal layout.130 Export an individual widget only when a named external compositor needs it,131 such as a page interleaving independent features, a layout or parallel route,132 or another route that consumes a subset.133- Keep feature UI, contracts, operations, and actions together. Create134 subdirectories only as responsibilities emerge.135- Keep feature-specific components, hooks, operations, and utilities inside136 their owner; do not turn global technical folders into dumping grounds.137- Give a workflow that genuinely spans multiple features an explicit138 orchestration owner. The orchestrator depends on public participant139 operations or injected ports; participants do not import one another. Pass a140 workflow command into participant UI when it must emit an intent, rather than141 creating a reverse import. Do not hide cross-feature behavior in a participant142 or generic shared folder.143- Move product-aware code to a shared product component only when it has144 multiple real consumers and no feature is its natural owner.145- Let a feature operation own its use-case contract and orchestration. Reuse146 central authorization, domain policy, data access, telemetry, and vendor147 adapters through headless domain or platform modules when those concerns span148 features. Recheck authorization at every trusted entry point; centralizing149 policy does not make callers trusted.150151## Protect module and runtime boundaries152153Prefer UI dependencies that flow from `app` to `features` to shared product154components to shared UI. Let server dependencies flow from feature operations155through domain or platform contracts to infrastructure. These are ownership156directions, not required folders. Hide storage, transport, credentials, and157vendor details behind the narrowest meaningful contract.158159- Expose intentional feature entry points; reject deep imports into another160 feature's internals.161- Compose peer features in `app` or an explicit workflow instead of importing162 one feature's UI or internals into another. When several features reuse a163 headless capability, move that capability below them rather than choosing one164 feature as the accidental owner.165- Keep server-only, client-only, and environment-neutral exports distinct. Do166 not re-export them through one ambiguous barrel.167- Use entry points such as `feature`, `feature/server`, and `feature/client`168 only when real consumers need those different runtime capabilities. Do not169 add symmetric barrels by convention.170- Mark sensitive modules with `server-only` and browser-bound modules with171 `client-only` when that makes invalid imports fail early.172- Give browser reads a browser-safe transport. A client query must not import a173 server-only feature operation; let a Route Handler or the repository's174 established client transport delegate to that operation.175- Pass only the data a Client Component needs across the server/client176 boundary, using an explicit serializable DTO or view model rather than raw177 storage or vendor objects.178- Prefer route-level composition when two features only need to appear or react179 to the same route state together.180- When the repository's scale makes boundary drift costly, enforce public181 entry points and forbidden import directions with its package, lint, or182 dependency checks. Do not introduce enforcement tooling merely because this183 skill was invoked.184185## Assign state deliberately186187Choose one authoritative owner for every state value:188189| State | Typical owner |190| ----------------------------------- | ----------------------------------- |191| Navigable or shareable view state | URL |192| Transient interaction state | Local Client Component |193| Persistent UI preference | Cookie or browser storage |194| Authoritative entity data | Server or data layer |195| Temporary optimistic projection | Client with explicit reconciliation |196| Continuously refreshed browser data | Client cache, when justified |197198- Put state in the URL when opening a copied URL should restore the view.199- Treat URL changes as navigation. Prefer links or forms where they fit, and200 centralize parameter semantics and dependent resets such as pagination.201- Do not introduce Context, a global store, or a client query cache merely to202 avoid deciding ownership.203- Allow derived or optimistic copies only when their source and reconciliation204 behavior are explicit.205206## Compose from the server207208- Start with Server Components. Introduce a Client Component only for browser209 APIs, effects, local interaction, event handlers, high-frequency updates, or210 optimistic feedback.211- Keep `"use client"` boundaries close to the interaction because every import212 below that boundary enters the client module graph.213- Let a server-rendered widget own what data it needs; let a feature operation214 own how that data is obtained.215- Start independent work independently. Keep sequential work only when one216 result truly depends on another.217- Use Server Functions or Actions for mutations initiated by the application.218 Use Route Handlers when an HTTP boundary is itself required. Do not use219 Server Actions as general read APIs.220- Validate and authorize inside every trusted mutation boundary, then make221 invalidation or refresh behavior explicit.222- Prefer links and forms for navigation and form-like interactions when they223 provide a useful baseline; add client behavior for material UX improvements.224225## Design loading, failure, and caching226227- Place Suspense around regions that can meaningfully load, stream, reveal, or228 refresh independently. Use route-level loading files when the whole segment229 shares that lifecycle.230- Keep a widget-shaped skeleton or fallback with the feature that owns the231 widget. Let the page place or compose that fallback; do not duplicate feature232 geometry in a route-level loading file unless the whole segment deliberately233 owns one coordinated shell.234- Match each fallback to the visual shape it replaces. Reset a boundary with a235 key only when the content identity truly changed.236- Model empty results, validation failures, denied access, rejected mutations,237 and known dependency failures as expected outcomes. Use `notFound()`,238 `redirect()`, and the repository's forbidden/unauthorized helpers at those239 boundaries.240- Reserve error boundaries and route error handling for unexpected failures.241 Define loading, empty, expected-failure, and unexpected-failure behavior242 before declaring the feature complete.243- Treat caching as part of each server operation's data contract, not as an244 incidental optimization. Keep the policy near the operation and define what245 is cached, what identifies the cache entry, how long it may be stale, what246 invalidates it, who owns that invalidation, and how users or tenants remain247 isolated.248- Treat a server-render cache and a browser query cache as separate249 representations. `queryClient.invalidateQueries` cannot refresh a Server250 Component.251- Detect whether the repository uses Cache Components. Separate invalidating252 cached data from making the current UI observe it. With Cache Components, use253 `updateTag` in a Server Action for immediate read-your-own-writes,254 `revalidateTag(tag, "max")` for stale-while-revalidate, and `revalidatePath`255 when the path is the intended invalidation scope. `refresh()` refreshes the256 client router from a Server Action but does not invalidate cached data.257 Without Cache Components, `fetch` is uncached by default; use the repo's258 `unstable_cache` / `revalidateTag` / `revalidatePath` primitives. Never259 assume historical `fetch` cache defaults still apply.260- Cookie `.set` / `.delete` are illegal during RSC render. Persist preferences261 only in a Server Function or Route Handler.262263## Verify the result264265Implement the smallest complete slice. Test pure route parsing and feature266operations directly; test navigation, rendering, mutation, and cache wiring at267their integration boundaries.268269Confirm that:270271- external inputs become typed state before reaching feature logic;272- pages compose rather than implement features;273- every behavior and state value has a clear authoritative owner;274- Server Components remain the default and client boundaries stay narrow;275- server-only code cannot enter the client graph;276- consumers use small, earned public feature APIs rather than internals;277- features compose shared UI rather than introducing competing primitives;278- shared product components exist only with multiple real consumers and no279 natural feature owner;280- infrastructure details remain below feature operations;281- reused domain and platform policy has a clear lower owner and does not create282 peer-feature dependencies;283- independent work is not accidentally serialized;284- loading and failure boundaries match meaningful user experiences;285- mutations validate, authorize, and invalidate deliberately;286- caching has explicit identity, freshness, invalidation ownership, and287 isolation semantics, and UI refresh is not mistaken for invalidation;288- abstractions and shared code represent real boundaries rather than ceremony.289290Correct unclear ownership or dependency direction before adding another layer.