feature-sliced-design
Official Feature-Sliced Design (FSD) v2.1 skill for applying the methodology to frontend projects. Use when the task involves organizing project structure with FSD layers, deciding where code belongs, placing static assets (images, icons, fonts, PDFs), grouping closely related sl
Install
npx skills add https://github.com/feature-sliced/skills/tree/master/feature-sliced-design
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install feature-sliced-skills@llmmart
git clone https://github.com/feature-sliced/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole feature-sliced/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Feature-Sliced Design (FSD) v2.1
Source: fsd.how | Strictness can be adjusted based on project scale and team context.
How to use this skill. For placement decisions, start with the decision tree in Section 2 and use the placement table in Section 3 as a quick reference. To check a structure for violations, use the rules in Section 4. To resolve same-layer cross-imports, use Section 7. For task-specific guidance, load only the relevant reference files from Section 10; do not preload the rest.
1. Core philosophy & layer overview
FSD v2.1 core principle: "Start simple, extract when needed."
The extraction rule
Place code in pages/ first. Duplication across pages is acceptable and
does not by itself require extraction to a lower layer. Extract only when
all three conditions hold:
- The same code is used in multiple places right now, not hypothetically.
- It has a reason to change that is independent of any one consumer.
- The boundary has a focused responsibility.
The six layers
Not all layers are required. Most projects can start with only shared/,
pages/, and app/. Add features/ and entities/ only when they provide
clear value. Do not create empty layer folders "just in case." The widgets/
layer is discouraged (see the callout below).
FSD uses 6 standardized layers, listed here from highest to lowest:
app/ → App initialization, providers, routing
pages/ → Route-level composition, owns its own logic
widgets/ → Reusable UI blocks (discouraged, see the callout below)
features/ → Reusable user interactions (see the extraction rule above)
entities/ → Reusable business domain models (see the extraction rule above)
shared/ → Infrastructure with no business logic (UI kit, utils, API client)
The official layer reference discourages using the Widgets layer, and this skill follows it. Widgets may seem useful for representing independent UI blocks. However, in real frontend code, UI blocks often include logic required for user flows, such as data fetching, state management, and event handling. In this case, the responsibilities of Features, which handle user flows, and Widgets, which handle UI blocks, can overlap, making the boundary between the two layers unclear.
Not creating a widget does not mean moving the block elsewhere untouched.
A screen-specific composition stays in pages; a reused action and the UI
to perform it go to features; context-free UI goes to shared; an
app-wide layout goes to app.
Discouraged is not deprecated: an existing widgets layer stays valid. See
references/layer-structure.md for that case and for layout placement.
The import rule
A module may only import from layers strictly below it. Cross-imports between slices on the same layer are forbidden, with one narrow exception in Section 7.
// Allowed
import { Button } from "@/shared/ui/Button"; // features → shared
import { useUser } from "@/entities/user"; // pages → entities
// Violation
import { loginUser } from "@/features/auth"; // entities → features
import { likePost } from "@/features/like-post"; // features → features
Note: The processes/ layer is deprecated in v2.1. For migration
details, read references/migration-guide.md.
2. Decision framework
When writing new code, follow this tree:
Step 1: Where is this code used?
- Used in only one page → keep it in that
pages/slice. - Used in 2+ pages but duplication is manageable → keeping separate copies in each page is also valid.
- An entity or feature with a single consumer → keep it at the consumer
(Steiger flags this as
insignificant-slice).
Step 2: Is it reusable infrastructure with no business logic?
The official layer reference draws the line for Shared like this: no business logic, but business-themed is fine (a company logo, a page layout), and so is UI logic (autocomplete, a search bar). Exchanging data with the backend and CRUD boilerplate are not business logic either. Business logic is a rule the product enforces on its own data, such as applying a discount to an order. If the code fits none of the exclusions and still does not clearly enforce a product rule, the term does not decide; go back to Step 1 and place it by where it is used.
- UI components →
shared/ui/ - Utility functions →
shared/lib/ - API client, route constants →
shared/api/orshared/config/ - Auth tokens, session management →
shared/auth/ - CRUD once several slices call it →
shared/api/(a single caller keeps it, see Step 1)
Step 3: Is it a complete user action that several consumers share, with a focused responsibility and a reason to change of its own?
- Yes →
features/ - Uncertain, single use, or speculative reuse → keep in the page.
Step 4: Is it a business domain model that several consumers share, with a focused responsibility and a reason to change of its own?
- Yes →
entities/ - Uncertain, single use, or speculative reuse → keep in the page.
Step 5: Is it app-wide configuration?
- Global providers, router, theme →
app/
Golden Rule: When in doubt, keep it in pages/. Extract only when the
extraction rule holds.
3. Quick placement table
| Scenario | Single use | Confirmed multi-use |
|---|---|---|
| User profile form | pages/profile/ui/ProfileForm.tsx |
features/profile-form/ |
| Product card | pages/products/ui/ProductCard.tsx |
entities/product/ui/ if the entity owns it |
| API request (read or CRUD) | pages/product-detail/api/fetch-product.ts |
shared/api/ (no domain rules) |
| Auth token/session | shared/auth/ |
shared/auth/ |
| Auth login form | pages/login/ui/LoginForm.tsx |
features/auth/ |
| Generic Card layout | shared/ui/Card/ |
|
| Modal manager | shared/ui/modal-manager/ |
|
| Modal content | pages/[page]/ui/SomeModal.tsx |
|
| Date formatting util | shared/lib/format-date.ts |
"Confirmed multi-use" means the extraction rule holds, not that a second
consumer appeared: two similar copies that keep drifting apart stay in
their pages (references/growth-walkthrough.md, Snapshot 1). Entity UI
carries the Section 6 caution even when the rule does hold.
4. Architectural rules (MUST)
These rules are the foundation of FSD. Violations weaken the architecture. If you must break a rule, ensure it is an intentional design decision and document the reason in code (a comment or ADR).
4-1. Import only from lower layers
app → pages → widgets → features → entities → shared.
Upward imports are forbidden. So are cross-imports between slices on the
same layer, except through the other slice's public API as a last resort
(Section 7, Strategy D).
4-2. Public API: every slice exports through index.ts
External consumers may only import from a slice's index.ts. Direct imports
of internal files are forbidden.
// Correct
import { LoginForm } from "@/features/auth";
// Violation: bypasses public API
import { LoginForm } from "@/features/auth/ui/LoginForm";
Shared layer: Shared has no slices. Define a separate public API per
segment (shared/ui/index.ts, shared/api/index.ts, etc.) rather than
one top-level shared/index.ts. This keeps imports from Shared
organized by intent.
Where one index over a segment's unrelated modules hurts bundling, give
each component, library, or controller folder its own index instead
(shared/ui/Button/index.ts as @/shared/ui/Button, shared/api/post/
as @/shared/api/post). That folder is then the boundary; reaching past
it (@/shared/ui/Button/Button.tsx) is still a violation. See
references/layer-structure.md for the shape.
Environment-specific entry points: a slice normally exposes one
index.ts, and ad-hoc variations are not recommended. If a single index
cannot preserve a runtime boundary, add an entry point such as
index.server.ts. See references/framework-integration.md.
4-3. No cross-imports between slices on the same layer
If two slices on the same layer need to share logic, follow the resolution order in Section 7. Never reach into another slice's internals.
4-4. Domain-based file naming (no desegmentation)
Name files after what they are for, the domain or concern they serve, not
after their technical role. Technical-role names like types.ts,
utils.ts, helpers.ts mix unrelated concerns in a single file and
reduce cohesion.
// BAD: technical-role naming
model/types.ts ← Which types? User? Order? Mixed?
model/utils.ts
// GOOD: domain-based naming
model/user.ts ← User types + related logic
model/order.ts ← Order types + related logic
api/fetch-profile.ts ← Clear purpose
4-5. No business logic in shared/
Shared contains only infrastructure: UI kit, utilities, API client setup,
route constants, assets. Business calculations, domain rules, and workflows
belong in entities/ or higher layers. Section 2, Step 2 says what counts.
// BAD: business logic in shared
// shared/lib/userHelpers.ts
export const calculateUserReputation = (user) => { ... };
// GOOD: move it to whoever owns the rule
// pages/profile/model/reputation.ts ← while the profile page owns it
// entities/user/model/reputation.ts ← once a user boundary is earned
export const calculateUserReputation = (user) => { ... };
5. Recommendations (SHOULD)
5-1. Pages first: place code where it is used
Place code in pages/ first. Extract to lower layers only when truly needed.
Extraction is a design decision that affects the whole project, so the
threshold should be high.
What stays in pages:
- Large UI blocks used only in one page
- Page-specific forms, validation, data fetching, state management
- Page-specific business logic and API integrations
- Code that looks reusable but is simpler to keep local
Evolution pattern: Start with everything in pages/profile/. Extract
the shared model to entities/user/ when a second page consumes it and
the extraction rule holds. A response type that several pages read is not
one of those cases: it stays in shared/api. Keep page-specific API calls
and UI in the page.
5-2. Be conservative with entities
The entities layer is highly accessible (almost every other layer can import from it), so changes propagate widely.
- Start without entities.
shared/+pages/+app/is valid FSD. Thin-client apps rarely need entities. - Do not split slices prematurely. Keep code in pages. Extract to entities only when the extraction rule holds.
- Business logic does not automatically require an entity. Keeping types
in
shared/apiand logic in the current slice'smodel/segment may be sufficient. - CRUD is infrastructure, not entities. Place it by the request
placement rule: with its consumer while there is one, in
shared/api/once several slices call it. - Place auth data in
shared/auth/orshared/api/. Tokens and login DTOs are auth-context-dependent and rarely reused outside authentication.
For detailed guidance on keeping the entities layer clean (when to skip
it entirely, how to isolate business contexts, why CRUD belongs in
shared/api), see references/excessive-entities.md.
5-3. Start with minimal layers
// Valid minimal FSD project
src/
app/ ← Providers, routing
pages/ ← All page-level code
shared/ ← UI kit, utils, API client
// Add layers only when an actual use case requires them:
// + features/ ← User-action boundaries that need one shared home
// + entities/ ← Domain boundaries that need one shared home
// (widgets/ is discouraged; see Section 1 for where that code goes instead)
5-4. Validate with the Steiger linter
Steiger is the official FSD linter. Key rules:
insignificant-slice: Flags a slice with no references, or with one, and suggests merging it into the layer above. Pages may hold a single reference, and so may slices used only fromapp/.excessive-slicing: Suggests merging or grouping when a layer has too many slices.
npm install -D @feature-sliced/steiger
npx steiger src
6. Anti-patterns (AVOID)
- Do not create entities prematurely. Data structures used in only one place belong in that place.
- Do not put CRUD in entities. Plain CRUD is
shared/api/. An operation that carries business rules is placed by who owns the rule, which may be an entity, a feature, or the page running the workflow. - Do not create a
userentity just for auth data. Tokens and login DTOs belong inshared/auth/orshared/api/. - Do not abuse
@x. It is a necessary compromise, not a recommended pattern. The notation is for the entities layer only, and only when boundary merge is genuinely impossible. Features and widgets handle cross-imports through strategies A through D (see Section 7). - Do not extract single-use code. A feature or entity used by only one page should stay in that page.
- Do not use technical-role file names. Use domain-based names (see Rule 4-4).
- Be cautious adding UI to entities. Entity UI tempts cross-imports from other entities. If you add UI segments to entities, only import them from higher layers (features, pages, app), never from other entities.
- Do not create god slices. Slices with excessively broad responsibilities
should be split into focused slices (e.g., split
user-management/intoauth/,profile-edit/,password-reset/). - Do not create a top-level
assets/segment. Place static assets next to the code that uses them; global stylesheets and fonts go toapp/. Seereferences/asset-handling.md.
7. Cross-import resolution
Cross-imports are a code smell, not an absolute prohibition. The right strategy depends on the layer and the situation.
Entities layer: prefer boundary merge, @x is last resort
Cross-imports in entities are usually caused by splitting entities too
granularly. Before reaching for @x, consider whether the boundaries
should be merged.
@x is a necessary compromise, not a recommended approach. Use it only
when boundaries genuinely cannot be merged, and document why. Overuse locks
entity boundaries together and increases refactoring cost.
Features and widgets: four strategies (A, B, C, D)
In features and widgets, choose based on context:
- Strategy A: slice merge. Two slices always change together → merge.
- Strategy B: push to entities. A shared domain responsibility → move it to the entity that owns it, keep UI in the feature.
- Strategy C: compose from upper layer (IoC). The parent (pages or app) imports both slices and connects them via render props, slots, or DI.
- Strategy D: Public API access. When reuse is genuinely unavoidable,
allow it only through the slice's
index.ts. Never reach intomodel/,store/, or internal files.
The @x notation is for the entities layer only. Features and widgets use
strategies A through D above.
Strictness depends on project context
Cross-imports are dependencies that are generally best avoided, but sometimes used intentionally. Strictness varies by project context:
- Early-stage products with heavy experimentation: allowing some cross-imports may be a pragmatic speed trade-off.
- Long-lived or regulated systems (fintech, large-scale services): stricter boundaries pay off in maintainability and stability.
If a cross-import is introduced, treat it as a deliberate choice and document the reasoning in code (a comment explaining why other strategies do not apply).
For detailed code examples of each strategy, read
references/cross-import-patterns.md.
8. Segments & structure rules
Standard segments
Segments group code within a slice by technical purpose:
ui/: UI components, styles, display-related codemodel/: Data models, state stores, business logic, validationapi/: Backend integration, request functions, API-specific typeslib/: Internal utility functions for this sliceconfig/: Configuration, feature flags
Layer structure rules
- App and Shared: No slices, organized directly by segments. Segments within these layers may import from each other.
- Pages, Widgets, Features, Entities: Slices first, then segments inside each slice.
- Slice groups (optional): A group folder may contain related slices on
the same layer for navigation purposes only. The group has no segments and
no public API. See
references/layer-structure.mdfor details.
File naming within segments
Always use domain-based names that describe what the code is about:
model/user.ts ← User types + logic + store
model/order.ts ← Order types + logic + store
api/fetch-profile.ts ← Profile fetching
api/update-settings.ts ← Settings update
If a segment has only one domain concern, the filename may match the slice
name (e.g., features/auth/model/auth.ts).
9. Shared layer guide
Shared contains infrastructure with no business logic. It is organized by segments only (no slices). Segments within shared may import from each other.
Allowed in shared:
ui/: UI kit (Button, Input, Modal, Card)lib/: Utilities (formatDate, debounce, classnames)api/: API client, route constants, CRUD helpers, base typesauth/: Auth tokens, login utilities, session managementconfig/: Environment variables, app settings- Assets live with the code that uses them, not in an
assets/segment. Seereferences/asset-handling.md.
Shared may contain application-aware code: route constants, API
endpoints, branding assets, and transport types such as ProductDTO.
It must never hold the business rules an entity or feature owns, nor
import from those layers.
10. Conditional references
Read the following reference files only when the specific situation applies. Do not preload all references.
When reviewing or reorganizing folder and file structure that already exists, deciding what goes inside a layer or slice, deciding where a page layout belongs, routing widget-like code to another layer, or grouping closely related slices into a parent folder for navigation (e.g., "where does this folder go", "how do I group these payment entities"): → Read
references/layer-structure.mdWhen setting up a new project from scratch (e.g., "set up an FSD project", "start a new app with FSD"), or when asked whether to add entities or features yet, or to show how a structure earns each layer over time rather than its finished shape: → Read
references/growth-walkthrough.mdWhen resolving cross-import issues between slices on the same layer, evaluating the
@xpattern, choosing between Strategy A/B/C/D for features and widgets, or deciding whether boundaries should be merged: → Readreferences/cross-import-patterns.mdWhen deciding whether to create or remove an entity, dealing with too many entities, evaluating whether to skip the entities layer entirely, placing CRUD operations, or isolating business contexts to avoid
@xchains: → Readreferences/excessive-entities.mdWhen deciding where to place static assets (images, icons, fonts, PDFs, stylesheets) for a single slice, for sharing across slices, or globally: → Read
references/asset-handling.mdWhen migrating from FSD v2.0 to v2.1, converting a non-FSD codebase to FSD, phasing out an existing widgets layer, or deprecating the processes layer: → Read
references/migration-guide.mdWhen integrating FSD with a specific framework (Next.js with App Router or Pages Router, React Router, Nuxt, Vite, Astro) for wiring routes to FSD pages, placing proxy/middleware and instrumentation files, structuring API route handlers, or configuring path aliases: → Read
references/framework-integration.mdWhen implementing authentication, type definitions, or API request handling as concrete code within FSD structure (token storage, login flow, DTO placement, where a request function lives): → Read
references/auth-and-api.mdWhen wiring state management (Redux slices, TanStack Query / React Query, including query factories, infinite scroll, Suspense mode, and
useMutationState) into FSD structure: → Readreferences/state-management.md
Files (skills)
-
evals
-
evals.json 19.6 KB
{ "$comment": "Placement cases for the feature-sliced-design skill, in the evals/evals.json layout that agent-skills-eval and skill-creator read. See README.md in this directory for how to run them. Every `source` path and `rule` fragment is checked by .github/scripts/validate-skills.mjs.", "skill_name": "feature-sliced-design", "evals": [ { "id": "auth-token", "prompt": "Where do I put the auth token and session helpers?", "expected_output": "shared/auth/ (or shared/api/). Do not create a user entity to hold the token; whether an existing current-user entity may store it is a separate question.", "assertions": [ "The output places the auth token and session helpers under shared/, in shared/auth or shared/api.", "The output does not create a new user entity to hold the token.", "If the output mentions an existing current-user entity, it treats whether that entity may store the token as a separate question rather than ruling it out." ], "why": "Tokens and session DTOs are infrastructure, not a domain model, and wrapping a login response in an entity is the common wrong turn.", "source": "feature-sliced-design/SKILL.md", "rule": "Section 2, Step 2; Section 6 anti-pattern on the user entity" }, { "id": "small-project-entities", "prompt": "I am starting a project with two pages. Should I create the entities and features layers now?", "expected_output": "No. app/ + pages/ + shared/ is valid FSD. Add entities or features only when a shared responsibility has a stable boundary its consumers must agree on; reuse alone does not require a layer.", "assertions": [ "The output answers no: the project should not create the entities or features layers now.", "The output states that app/, pages/, and shared/ alone is a valid FSD structure.", "The output conditions adding entities or features on a shared responsibility with a stable boundary, and does not treat reuse across pages as sufficient on its own." ], "why": "A common failure mode: small projects split into every layer up front.", "source": "feature-sliced-design/SKILL.md", "rule": "Section 5-2 'Start without entities'; Section 5-3" }, { "id": "plain-reusable-request", "prompt": "getUserById just wraps GET /users/:id and every page calls it. Does it belong in entities/user/api/?", "expected_output": "No. shared/api/. Plain resource access stays infrastructure however many consumers call it. A request moves into an entity only when an established entity boundary owns that domain responsibility.", "assertions": [ "The output answers no and places getUserById in shared/api.", "The output keeps plain resource access in shared regardless of how many pages call it.", "The output moves a request into an entity only when an established entity boundary owns that domain responsibility." ], "why": "Guards the two-question placement rule. The old bullet list sent this exact function to entities.", "source": "feature-sliced-design/references/auth-and-api.md", "rule": "Request placement rule, Question 2" }, { "id": "single-page-request", "prompt": "Dashboard stats are fetched only on the dashboard page. Where does the request go?", "expected_output": "pages/dashboard/api/. It stays with its only consumer.", "assertions": [ "The output places the dashboard stats request in pages/dashboard/api/.", "The output does not move the request to shared/api or to an entity while the dashboard page is its only consumer." ], "why": "Question 1 must be asked before Question 2, or pages-first breaks.", "source": "feature-sliced-design/references/auth-and-api.md", "rule": "Request placement rule, Question 1" }, { "id": "same-layer-import", "prompt": "features/profile needs something from features/auth. Is that allowed?", "expected_output": "Generally no. Try strategies A to C first. If the documented last resort is genuinely necessary, import only through the other slice's public API, never its internals.", "assertions": [ "The output says a direct import from features/auth into features/profile is generally not allowed.", "The output recommends resolving the dependency first by merging the slices, moving the shared responsibility into an entity, or composing from an upper layer.", "The output keeps a last resort reachable: if the direct import is necessary, it goes through the other slice's public API and never through its internals." ], "why": "The MUST rule reads as an absolute ban; the documented escape hatch must stay reachable.", "source": "feature-sliced-design/references/cross-import-patterns.md", "rule": "Strategy D; SKILL.md Rules 4-1 and 4-3" }, { "id": "shared-ui-import", "prompt": "How do I import the Button from the shared UI kit?", "expected_output": "Through the Shared UI public API: @/shared/ui when the segment index exports Button, or @/shared/ui/Button when Button has been given its own index. Never an internal file such as @/shared/ui/Button/Button.tsx.", "assertions": [ "The output imports Button through the shared UI public API: @/shared/ui from the segment index, or @/shared/ui/Button when Button has its own index.", "The output does not reject either @/shared/ui or @/shared/ui/Button as incorrect.", "The output never imports an internal file such as @/shared/ui/Button/Button.tsx." ], "why": "Rule 4-2 makes the segment index the default and a per-component index the fallback for tree-shaking, so both imports are correct and only reaching past the boundary is not.", "source": "feature-sliced-design/SKILL.md", "rule": "Rule 4-2" }, { "id": "business-themed-in-shared", "prompt": "Can the company logo component and an autocomplete input live in shared/ui?", "expected_output": "Yes, as long as they encode no business rules and no slice-specific behavior. Business-themed presentation and generic UI interaction logic may live in shared/ui; business logic may not.", "assertions": [ "The output answers yes: both the company logo component and the autocomplete input may live in shared/ui.", "The output conditions this on the components encoding no business rules and no slice-specific behavior.", "The output does not treat business-themed presentation or generic UI interaction logic as business logic that must leave shared." ], "why": "Rule 4-5 is easy to over-apply. Shared excludes business logic, but business-themed code and UI logic are explicitly allowed, and an agent that flattens the rule removes both.", "source": "feature-sliced-design/SKILL.md", "rule": "Section 2, Step 2" }, { "id": "new-project-three-pages", "prompt": "Set up an FSD structure for a shop with home, product, and search pages. Home and search both call the same fetchProducts request, and every page reads the same ProductDTO.", "expected_output": "app/, pages/ with three slices, shared/. No entities or features. ProductDTO and the shared fetchProducts go in shared/api; a request only one page calls stays in that page's api/ segment.", "assertions": [ "The output proposes app/, pages/ with home, product, and search slices, and shared/, with no entities, features, or widgets layer.", "The output places ProductDTO and fetchProducts in shared/api.", "The output keeps a request that only one page calls in that page's api/ segment." ], "why": "The finished-structure reflex. Fails if any layer beyond three appears unprompted.", "source": "feature-sliced-design/references/growth-walkthrough.md", "rule": "Snapshot 0 and Snapshot 1; auth-and-api.md request placement rule" }, { "id": "diverged-rule", "prompt": "Two pages each compute whether a product is on sale, and one copy is now out of date. What should change?", "expected_output": "Move the rule to entities/product/model. Leave the ProductDTO in shared/api and the badge UI in the pages.", "assertions": [ "The output moves the on-sale rule to entities/product/model.", "The output leaves ProductDTO in shared/api.", "The output leaves the badge UI in the pages rather than moving it into the entity." ], "why": "Reuse alone did not open entities; a rule that must agree with itself does. Also checks that the DTO does not follow the rule into the entity.", "source": "feature-sliced-design/references/growth-walkthrough.md", "rule": "Snapshot 2" }, { "id": "app-header-placement", "prompt": "New project. Where does the app-wide header with navigation and the user menu go?", "expected_output": "The header composition goes in app/ as an application-level layout, not a new widgets/ slice. Its parts keep their own ownership: an already-established feature may supply a reused action, an established entity may supply domain UI, context-free UI comes from shared. A section only one page renders stays in that page.", "assertions": [ "The output places the header composition in app/ as an application-level layout.", "The output does not create a widgets/ slice for the header.", "The output sources the header's parts from their existing owners (an established feature for a reused action, an established entity for domain UI, shared for context-free UI) without creating new slices for them.", "The output keeps a section that only one page renders in that page." ], "why": "The header is the textbook widgets example. Placement is decided by scope, and the skill discourages opening widgets for it.", "source": "feature-sliced-design/references/layer-structure.md", "rule": "Where should layouts be placed?; SKILL.md Section 1 widgets callout" }, { "id": "top-level-assets-segment", "prompt": "Should I create src/assets/ to hold all the images and icons?", "expected_output": "No. Keep an asset with the slice that owns it. A presentation asset several slices must share goes to the shared UI module that owns it, global styles and imported fonts go with app-level code, and files served as-is go in the framework's configured public directory.", "assertions": [ "The output answers no to a top-level src/assets/ folder.", "The output keeps each asset with the slice that owns it, and sends a presentation asset several slices share to the shared UI module that owns it.", "The output puts global styles and imported fonts with app-level code, and files served as-is in the framework's configured public directory." ], "why": "A type-based assets folder is the common default and the official guidance calls it not recommended.", "source": "feature-sliced-design/references/asset-handling.md", "rule": "Caution; Decision tree" }, { "id": "nextjs-app-layer-name", "prompt": "Next.js App Router project. Where does the FSD app layer go, and what is it called?", "expected_output": "src/_app/, with src/_pages/ for the pages layer. The Next.js app/ routing folder stays at the project root and only re-exports from FSD pages.", "assertions": [ "The output names the FSD app layer src/_app/ and the FSD pages layer src/_pages/.", "The output keeps the Next.js app/ routing folder at the project root and has it only re-export from FSD pages.", "The output does not move the Next.js routing folder or leave the FSD layers unprefixed under src/." ], "why": "An older pattern kept the FSD layers unprefixed under src/ and moved the routing folder instead. Both existed in the wild.", "source": "feature-sliced-design/references/framework-integration.md", "rule": "Next.js; Projects on the previously recommended pattern" }, { "id": "widgets-not-deprecated", "prompt": "Our FSD 2.0 project already uses widgets. Does 2.1 require us to remove the layer?", "expected_output": "No. Widgets are discouraged for new adoption, not deprecated, so an existing layer stays valid and keeps working. processes/ is the layer 2.1 deprecates. Phasing widgets out is optional.", "assertions": [ "The output answers no: FSD 2.1 does not require removing an existing widgets layer.", "The output distinguishes discouraged from deprecated, and names processes/ as the layer 2.1 deprecates.", "The output presents phasing widgets out as optional." ], "why": "Discouraged is not deprecated. The reader most likely arrived from 2.0 already using widgets, and the guide calls this migration optional.", "source": "feature-sliced-design/references/migration-guide.md", "rule": "Phasing out a small widgets layer (optional); SKILL.md Section 1 widgets callout" }, { "id": "phase-out-widget-placement", "prompt": "We decided to remove our small widgets layer. We have an app shell, a section only one page renders, a stable user action reused on several pages that must behave the same in each, and a generic UI block. Where does each go?", "expected_output": "App shell to app; the one-page section inlines into that page; the reused action and its UI to features; the context-free UI block to shared/ui. A widget that only composes features moves up to the page or the route layout.", "assertions": [ "The output moves the app shell to app/.", "The output inlines the section only one page renders into that page.", "The output moves the reused user action and its UI to features/.", "The output moves the generic UI block to shared/ui.", "The output moves a widget that only composes features up to the page or the route layout." ], "why": "Splits the routing table off the deprecated-versus-discouraged question so a failure says which half is wrong.", "source": "feature-sliced-design/references/migration-guide.md", "rule": "Phasing out a small widgets layer (optional)" }, { "id": "entity-selector-root-state", "prompt": "In entities/todo/model/todo.ts I am writing selectTodos. RootState is exported from app/providers/store.ts. How do I type the selector?", "expected_output": "Type it against only the state it reads, e.g. (state: { todos: TodoState }) => state.todos.items. Must not import RootState from app/: entities cannot depend on a higher layer. Type against RootState at the app layer if that guarantee is needed.", "assertions": [ "The output types the selector against only the state it reads, for example (state: { todos: TodoState }) => state.todos.items.", "The output does not import RootState from app/ inside entities/todo.", "The output explains that entities cannot depend on a higher layer, and offers typing against RootState at the app layer as the alternative." ], "why": "Regression case: the skill's own example once made this upward import. Redux tutorials type selectors with RootState, so an agent reaches for it unless the dependency rule is spelled out.", "source": "feature-sliced-design/references/state-management.md", "rule": "Business-entity slice in entities; SKILL.md Rule 4-1" }, { "id": "similar-product-cards", "prompt": "Home and search both render a ProductCard. The home card shows a recommendation reason, the search card shows a query-match snippet. Should I extract ProductCard to entities/product/ui?", "expected_output": "No, not because both pages have one. They are not the same component, they change for their own reasons, and separate page-local copies are valid. Extract only if the two must agree and need one home.", "assertions": [ "The output answers no: it does not extract ProductCard to entities/product/ui.", "The output does not treat two pages having a component with the same name as a reason to extract.", "The output keeps separate page-local copies as valid because the two cards change for their own reasons, and extracts only if they must agree and need one home." ], "why": "The whole skill decides extraction on a shared responsibility, not on a count or a matching filename, and every other case tests the positive direction.", "source": "feature-sliced-design/references/growth-walkthrough.md", "rule": "Snapshot 1, 'The second card is a copy, not an extraction'" }, { "id": "single-use-user-action", "prompt": "Only the checkout page has a coupon form right now. Should I create features/apply-coupon?", "expected_output": "No, not yet. Keep the form, its request, and its state in the checkout page while it has one consumer. Extract when another consumer appears and the action has to behave the same in both.", "assertions": [ "The output answers no: it does not create features/apply-coupon now.", "The output keeps the coupon form, its request, and its state in the checkout page while that page is the only consumer.", "The output conditions extraction on another consumer appearing and the action having to behave the same in both." ], "why": "Guards the verb-equals-feature reflex, the features counterpart of small-project-entities, which no case covered.", "source": "feature-sliced-design/SKILL.md", "rule": "Section 2, Step 3; Section 5-1" }, { "id": "user-entity-does-not-own-token", "prompt": "We now have entities/user because profile identity is reused across the app. Should I move the access and refresh tokens out of shared/auth into entities/user?", "expected_output": "shared/auth remains the default. Entity-owned credentials are also valid, but only when an already-established user or session entity genuinely owns that authentication state; the entity existing, or profile data being reused, is not enough on its own.", "assertions": [ "The output keeps shared/auth as the default home for the access and refresh tokens.", "The output does not move the tokens into entities/user merely because the entity exists or profile data is reused.", "The output allows entity-owned credentials only when an already-established user or session entity owns that authentication state." ], "why": "The line between reusable user-domain state and authentication infrastructure moved twice while these references were being aligned, so pin where it landed.", "source": "feature-sliced-design/references/auth-and-api.md", "rule": "When to use shared/auth vs a user entity" }, { "id": "single-consumer-crud", "prompt": "Only the settings page calls updateNotificationSettings. It is a plain PATCH with no business rules. Should it go in shared/api because it is CRUD?", "expected_output": "No. Keep it in pages/settings/api/ while that page is its only consumer. CRUD not being entity material does not make it Shared material; it moves to shared/api once it is genuinely shared.", "assertions": [ "The output answers no and keeps updateNotificationSettings in pages/settings/api/.", "The output does not send the request to shared/api on the grounds that it is CRUD.", "The output moves the request to shared/api once more than one consumer calls it." ], "why": "Guards pages-first request ownership against the shortcut that CRUD always goes to shared/api, which three passages stated before this alignment.", "source": "feature-sliced-design/references/auth-and-api.md", "rule": "Request placement rule, Question 1" } ] } -
README.md 5.3 KB
# Skill evaluation cases `evals.json` records the placement answers this skill is supposed to produce. The validator keeps the file honest; a person or a harness grades the answers. The file uses the `evals/evals.json` layout that Anthropic's `skill-creator` and [agent-skills-eval](https://github.com/darkrishabh/agent-skills-eval) read, so the same cases run in a judge-model harness without conversion. The `why`, `source`, and `rule` fields are this repository's additions; harnesses ignore them and the validator checks them. ## Why this exists The other checks in this repository verify that the documents are well formed: the body stays under the line limit, every reference path resolves, no reference is orphaned. None of them verify the thing the skill is for, which is whether an agent reading it places code correctly. That gap is not theoretical. Several rules in this skill were changed because two passages decided the same question on different grounds, and nothing in CI noticed. A case list is the cheapest way to catch the next one. ## Running the cases There is no semantic grader in CI, because comparing an answer to `expected_output` needs a model or a person. ### With a harness From the repository root, with an OpenAI-compatible API key in the environment: ```bash npx agent-skills-eval . --target <model> --judge <model> --baseline ``` It runs every prompt twice, with and without the skill in context, grades both against the case's assertions, and writes a report under `agent-skills-workspace/`. Read the `--baseline` column first. A case that passes without the skill is guarding a mistake the model does not make, so consider dropping it. A case that passes only with the skill shows where the skill changes the answer. The harness puts `SKILL.md` and every file under `references/` into context at once, so a pass shows that the documents decide the case correctly. Whether `SKILL.md` sends an agent to the right reference is a separate question the harness cannot answer, because an agent reads the skill one file at a time. Check that by hand. ### By hand 1. Start an agent session with only this skill installed. 2. Send one `prompt` verbatim. Do not add context; the point is to see what the skill alone produces. 3. Check the answer against each assertion. Judge the placement, not the wording. 4. On a mismatch, read the file named in `source` and check whether the rule is absent, ambiguous, contradicted elsewhere, or whether the case itself no longer describes the behavior the skill intends. Fix whichever one is wrong. Never edit `expected_output` just to match what the model said. Start a fresh session per case. A previous answer in the same conversation will steer the next one. ## Adding a case Add an object to `evals` with all seven fields: | Field | Meaning | | --- | --- | | `id` | kebab-case, unique | | `prompt` | what the user types, verbatim | | `expected_output` | the placement, plus what must not happen if that matters | | `assertions` | `expected_output` split into conditions a judge can grade one at a time | | `why` | what regression this case guards against | | `source` | repo-relative path to the primary file that decides it | | `rule` | the passage that decides it, as `;`-separated fragments; name a passage from another file too when the decision leans on one | Each assertion states one condition on the answer, phrased as "The output ...". The first one names the placement. Each thing that must not happen gets an assertion of its own, so a failed run names the condition that broke. Two to four per case is usual; a routing case that asks about several items gets one per item. `source` names one file, the one to open first on a mismatch, even where the decision is settled by more than one passage. It must point at a file that exists, and `node .github/scripts/validate-skills.mjs` enforces that, so no case can cite a file that has been deleted. The validator also resolves every fragment of `rule` against the skill's documents, so a renamed heading or renumbered section fails the build instead of leaving a case pointing at nothing. A fragment resolves against `source` unless it names another file (`SKILL.md`, `auth-and-api.md`), and it must contain at least one of: - a numbered reference such as `Section 2, Step 3`, `Rule 4-2`, `Strategy D`, `Snapshot 1`, or `Question 2` - a phrase in single or double quotes that appears verbatim in the file - the text of a heading or bold label in the file, such as `Decision tree` `Section N` and `Rule N-M` always mean a numbered heading in `SKILL.md`. Free prose after a numbered reference is allowed and not checked, so `Section 6 anti-pattern on the user entity` is fine. Write a case when it guards a mistake that is actually likely: a rule two readings could defend, a regression that has already happened once, or a convention from outside FSD that an agent will reach for anyway. Several cases here are the third kind, where the rule is plain and the pull toward breaking it is what needs holding down. Keep a case on one architectural decision where you can. A prompt that checks several independent placements fails as one result, and then the failure does not say which rule broke. Split it instead. A case that restates an obvious rule without guarding a realistic mistake costs a run and catches nothing.
-
-
references
-
asset-handling.md 6.4 KB
# Asset Handling How to place static assets (images, icons, fonts, PDFs, stylesheets) inside an FSD project. Assets follow the same ownership rules as code: keep each one with the module whose lifecycle it shares, rather than grouping them by file type or by how many places use them. > **Caution:** A custom top-level `assets` segment that aggregates all static > files is **not recommended**. It violates the FSD principles of high > cohesion and locality of changes. Place assets where they are used. ## Decision tree 1. **Owned by one slice?** Keep it in that slice, next to the segment that consumes it. A presentation asset usually lands in `ui/`; one coupled to domain logic can live in `model/`. 2. **Must several consumers share one authoritative copy (a logo, the placeholder icon)?** Put it with the shared module that owns it, which for a presentation asset is `shared/ui/`. Two assets that merely look alike today and will change for their own reasons stay local. 3. **Global stylesheet, font, or app-level resource?** Place it in the `app/` layer, by convention `app/styles/` and `app/fonts/`. 4. **Served as-is (favicon, robots.txt)?** Use the framework's `public/` folder. The `public/` folder is not part of FSD and does not conflict with FSD layers. ## Slice-specific assets When an asset belongs to one page, widget, or feature, keep it inside that slice. The asset lives next to the component that renders it: ```text pages/ home/ ui/ hero-image.jpg ← Used only by HomePage HomePage.tsx index.ts ``` If a slice uses many static images, group them in a subfolder of `ui/`: ```text pages/ home/ ui/ previews/ cake.jpg pizza.jpg sushi.jpg HomePage.tsx index.ts ``` ### Non-UI assets Some assets are not part of the UI but are coupled to business logic. For example, a PDF template used to generate invoices. Place these in the `model/` segment alongside the logic that consumes them, not in `ui/`: ```text features/ billing/ model/ invoice-template.pdf ← Coupled to create-invoice.ts create-invoice.ts index.ts ``` The principle is locality of changes: if you delete the slice, every file it owns goes with it. An asset that lives in business logic should sit next to that logic. ## Shared assets When several slices must share one authoritative copy of an asset, move it out of the slices. A presentation asset goes to `shared/ui/`, in a topical subfolder or next to the single shared component that uses it; anything else goes with the shared module that owns it: ```text shared/ ui/ placeholders/ ← Reused placeholder images cake.jpg pizza.jpg Dropdown.tsx chevron.svg ← Used only by Dropdown, kept next to it ``` A single icon used by exactly one component in the UI kit stays next to that component. A library of icons or images reused across many components goes in a topical subfolder. ## Global assets Global stylesheets and fonts belong in the `app/` layer because they are imported by the application entrypoint, not by individual slices: ```text app/ styles/ reset.css global.css fonts/ inter.woff2 main.ts ``` Theme variables, CSS resets, and font registrations are app-wide concerns. `styles/` and `fonts/` are conventional App folder names, not standardized ones; `references/layer-structure.md` covers how App segments are named. ## Public folder Most frameworks and build tools provide a static-asset directory at the project root. Files there are served as-is, without bundling or hashing. The default is `public/` at the project root in Vite, Next.js, Nuxt, and Astro. Whether it can be moved is framework-specific: Vite and Astro both expose a `publicDir` option, while Next.js documents the project root. `public/` is not part of FSD. It does not collide with FSD layers and does not need to live under `src/`. Use it for files that must be served at fixed URLs: favicon, `robots.txt`, `sitemap.xml`, OG images, and similar. A few of these have framework conventions of their own, such as Next.js metadata files, and those win over the generic rule. > **Where this comes from.** The official assets guide says Astro has no > option to move its public folder. Astro documents `publicDir` with a > default of `./public` and an example of changing it, so this section > follows the framework. ```text public/ favicon.ico robots.txt og-image.png src/ app/ pages/ shared/ ``` Where the framework lets that directory be configured, treat wherever it points as a framework boundary, not as an FSD segment. ## Summary table | Asset | Location | | -------------------------------------- | ----------------------------------------- | | Asset owned by one slice | Inside that slice, next to its consumer | | PDF or template tied to business logic | Inside the slice's `model/` segment | | Presentation asset several must share | `shared/ui/`, with the module owning it | | Icon used by exactly one shared kit UI | Next to that component in `shared/ui/` | | Global CSS reset, theme variables | `app/styles/` | | Web fonts | App layer when bundled, else public dir | | Favicon, robots.txt, sitemap | Framework convention, else public dir | ## Anti-patterns - **Do not create a top-level `assets/` segment** that holds all images, fonts, and icons. It breaks cohesion and forces consumers to import from a folder unrelated to the code they are working on. - **Do not extract a slice-owned asset to `shared/` "in case".** Move it when shared ownership is real and one authoritative copy should serve several consumers. - **Do not place CSS modules in an `assets/` folder.** A component's stylesheet belongs next to that component in `ui/`. - **Do not name an FSD segment `public`.** The framework's `public/` folder is reserved and lives outside `src/`. - **Do not separate an asset from its owning module without a boundary reason.** A page that ships a hero image keeps it, so removing the page removes the image. A fixed public URL is such a reason; convenience is not. ## See also - `references/layer-structure.md`: segment rules and layer organization - [Desegmentation](https://fsd.how/docs/guides/issues/desegmented/): why technical-role grouping (including a generic `assets/` segment) hurts cohesion -
auth-and-api.md 15.4 KB
# Authentication, Types, and API Requests Concrete code patterns for authentication, type definitions, and API request handling within FSD structure. State management patterns (Redux, TanStack Query) are in `references/state-management.md`. Code samples are React; the placement rules are framework-agnostic. ## Authentication Auth is one of the most common sources of confusion in FSD. The key question is: what goes in `shared/`, what goes in `features/` or `pages/`? ### Auth data: `shared/auth/` or `shared/api/` Credential storage, authentication-session plumbing, and the API-client helpers around them are **infrastructure**, not business logic. Keep them in shared. The login flow itself, its form state, validation and error handling, is a user action and does not come with them: ```typescript // shared/auth/token.ts const TOKEN_KEY = "auth_token"; export const getToken = () => localStorage.getItem(TOKEN_KEY); export const setToken = (t: string) => localStorage.setItem(TOKEN_KEY, t); export const clearToken = () => localStorage.removeItem(TOKEN_KEY); // shared/auth/session.ts export interface Session { userId: string; email: string; role: "admin" | "user" } // useSession depends on the auth provider (React Context, Zustand, etc.) export const useSession = (): Session | null => { /* ... */ }; ``` The `shared/auth/index.ts` re-exports from these files following the standard public API pattern. Which of the two: the token can sit in `shared/api` next to the client, where every request function can reach it directly. When token management grows past that (refresh, expiry, invalidation), the official Auth guide separates the responsibilities: requests and the API client stay in `shared/api`, and the token store with its management logic moves to `shared/auth`. ### Auth UI: pages (single use) or features (multi-use) Place the login form in the slice that consumes it. Single-use (only on the login page) goes in `pages/login/`; multi-use (dedicated page + modal login) goes in `features/auth/`: ```text pages/login/ ← Single-use ui/{LoginPage,LoginForm}.tsx model/login.ts ← Form state, validation api/login.ts ← POST /auth/login index.ts features/auth/ ← Multi-use: signing in ui/LoginForm.tsx model/auth.ts api/login.ts index.ts features/register/ ← Signing up, its own use case ui/RegisterForm.tsx model/register.ts api/register.ts index.ts ``` ### Dialog for login If you need a login dialog that can be reused across multiple pages, you can implement it as a **feature** responsible for the login user action and flow. A login dialog typically includes logic such as form state management, input validation, authentication requests and error handling. These responsibilities belong in the `features` layer because they handle user actions and flows. ```text features/ auth/ ui/LoginDialog.tsx model/ api/ index.ts ``` When multiple pages need the login dialog, they can import and use the feature from each page or from the route configuration in `app`. A component responsible only for the common dialog UI and basic interactions can be placed in `shared/ui`. This component should not include login-specific logic such as authentication requests, input validation or authentication state management. The UI and logic required for login should be managed in `features/auth`. When necessary, `LoginDialog` can be implemented by composing the dialog component from `shared/ui`. ```text shared/ ui/ modal/ Modal.tsx index.ts features/ auth/ ui/LoginDialog.tsx model/ api/ index.ts ``` ### When to use shared/auth vs a user entity The official Auth guide presents two valid storage locations: **In Shared** (`shared/auth` or `shared/api`) and **In Entities** (a `user` entity). **In Pages/Widgets** is not recommended. `shared/auth` is the default for tokens, refresh and expiry handling, and the rest of the authentication session. Keep them there unless something else already owns that state. A `user` or `session` entity may own auth state, token included, when that entity is an established boundary that genuinely owns it. Ownership is what decides. An entities layer existing, a `user` entity existing, and profile data being reused are not reasons to move credentials. ```text // Path A: shared/auth (simpler default) shared/auth/session.ts ← userId, email, role, token // Path B: an established user-domain boundary that owns this state entities/user/ model/ current-user.ts ← Current authenticated user + token user.ts ← Generic user type api/get-current-user.ts index.ts ``` For the entity approach, the API client in `shared/api` cannot import from `entities/`. The official guide describes three solutions: pass the token manually, expose it through a context with the key kept in `shared/api`, or inject the token into the API client when the entity store updates. A `user` entity created **only** to wrap a login response is premature. `references/excessive-entities.md` explains what that costs. ### In Pages/Widgets (not recommended) It is not recommended to place the token store in `pages` or in a specific `features` slice. Tokens are not state that belongs only to a specific page or a single user action. They are application-wide state used by multiple authenticated API requests and user flows. For example, if the token store is placed in `features/auth`, another feature cannot directly import it. Different feature slices on the same layer should remain independent from one another. Similarly, if the token store is placed in `pages`, modules on lower layers cannot access it. This makes it difficult to reuse the token store in authenticated API requests or other user flows. Place the token store in `shared` or in an `entities` slice representing the current user or session, according to the criteria described above. ### Logout and token invalidation Most applications do not provide a separate page exclusively for logout. Instead, logout functionality is made available wherever it is needed, such as in a header, settings screen, or user menu. Logout generally consists of the following steps. 1. Send an authenticated logout request to the backend. For example, `POST /logout`. 2. Reset the token store. Remove both the access token and refresh token. 3. Reset the current user information and authentication state when necessary. 4. Navigate to the login page or another screen when necessary. The location of the logout request should be determined by the project's API organization and the scope in which the request is reused. If all API endpoints are managed in `shared/api`, authentication-related requests such as login, logout, and token refresh can be placed together. ```text shared/ api/ client.ts endpoints/ login.ts logout.ts refresh-token.ts index.ts ``` If the logout request is used only as part of a specific logout flow, it can be placed in the `api` segment of `features/logout`. If logout is reused across multiple screens and represents an independent user flow that includes token cleanup, user state cleanup, and error handling, the flow can be extracted into `features/logout`. Navigation is the exception. `features/logout` must not import the router from `app/`, which would be an upward import (Rule 4-1). Let the page or route configuration navigate after the action resolves, or pass the navigation callback into the feature: ```typescript // pages/settings/ui/SettingsPage.tsx const logout = useLogout(); const onLogout = async () => { await logout(); navigate("/login"); }; ``` ```text features/ logout/ api/logout.ts ui/LogoutButton.tsx index.ts ``` If logout requires its own state or reusable processing logic, a `model` segment can be added. There is no need to create unused segments in advance for a simple logout feature. `features/logout` coordinates tasks such as sending the logout request and resetting the token store as a single user flow. The token store itself and the token management logic should remain in the previously selected location under `shared` or `entities`. On the other hand, if the logout logic is simple and used in only one or two places, it does not necessarily need to be extracted into a separate feature. It can be composed directly in the page or route configuration where it is used. > Slice names should be based on user actions and flows rather than the UI > location where they are displayed. Therefore, even when logout is triggered > from a header, `features/logout` is more appropriate than `features/header` > when the behavior is extracted as an independent user flow. ### Automatic logout The token store and current user state should be reset when the client's authentication state can no longer be maintained, such as in the following cases: - The user requests to log out. - The refresh token has expired or is invalid, causing the token refresh request to be rejected. If the authentication state is not reset, the UI may appear as though the user is still logged in while authenticated API requests continue to fail. Even if the logout request fails, the client can still reset the token store and current user state. However, the server-side session or refresh token may not have been invalidated, so the backend authentication policy should also be taken into account. > If tokens are managed in an entity representing the current user or session, > the token reset logic can be placed in the slice's `model` segment. If tokens > are managed on the Shared layer, they can be separated into a module > responsible for authentication, such as `shared/auth`. The refresh failure is usually detected by the API client in `shared/api`, which cannot reach an entity to clear its state: that would be an upward import. Report the failure upward instead, through the callback, event, or context the official guide already uses to hand the token down, and let the layer that owns the state do the resetting. The same wiring that gets the token into the client carries the failure back out. ## Type definitions ### Where to define types The location of type definitions follows the same rules as any other code: | Type scope | Location | | --- | --- | | API response/request shapes shared across the app | Domain-named files in `shared/api/` (e.g., `shared/api/product.ts`) | | Types for a specific entity's domain model | `entities/<name>/model/<name>.ts` | | Types used only within one page | `pages/<name>/model/<name>.ts` | | Types used only within one feature | `features/<name>/model/<name>.ts` | | Generic utility types (e.g., `Nullable<T>`) | Purpose-named files in `shared/lib/` (e.g., `shared/lib/nullable.ts`) | Per Rule 4-4 (domain-based file naming), avoid grouping all types in `types.ts` or `utils.ts`. A file named `types.ts` cannot answer "types for what?" without inspection; a file named `product.ts` can. ### Example: API types in shared ```typescript // shared/api/product.ts: raw API response shapes export interface ProductDTO { id: string; name: string; price: number; category: string; createdAt: string; } ``` ### Example: domain types in entities ```typescript // entities/product/model/product.ts: the shape the domain works with import type { ProductDTO } from "@/shared/api"; export interface Product { id: string; name: string; price: number; listPrice: number; inStock: boolean; } export const fromProductDTO = (dto: ProductDTO): Product => ({ id: dto.id, name: dto.name, price: dto.price, listPrice: dto.listPrice, inStock: dto.stock > 0, }); // a rule, not a stored flag: it follows the price rather than a snapshot export const isOnSale = (product: Product) => product.price < product.listPrice && product.inStock; ``` **Key principle:** Raw API shapes go in `shared/api/`. A domain model stays with its current consumer until an entity boundary has been earned; once it has, the types and rules that entity owns live in its `model/`. If you only need the raw shape, do not create an entity just for types. ## API request handling ### Basic pattern: API calls in the consuming slice ```typescript // pages/product-detail/api/fetch-product.ts import { apiClient, type ProductDTO } from "@/shared/api"; export const fetchProduct = (id: string): Promise<ProductDTO> => apiClient.get(`/products/${id}`).then((r) => r.data); ``` ### Shared API client setup ```typescript // shared/api/client.ts import axios from "axios"; import { getToken } from "@/shared/auth"; export const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_URL }); apiClient.interceptors.request.use((config) => { const token = getToken(); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); ``` The `shared/api/index.ts` re-exports from these files, so consumers import `apiClient` and the DTO types from `@/shared/api` rather than reaching into `client.ts` or `product.ts` (Rule 4-2). ### CRUD helpers in shared ```typescript // shared/api/create-crud-api.ts import { apiClient } from "./client"; export const createCrudApi = <T>(resource: string) => ({ getAll: () => apiClient.get<T[]>(`/${resource}`).then((r) => r.data), getById: (id: string) => apiClient.get<T>(`/${resource}/${id}`).then((r) => r.data), create: (data: Partial<T>) => apiClient.post<T>(`/${resource}`, data).then((r) => r.data), update: (id: string, data: Partial<T>) => apiClient.put<T>(`/${resource}/${id}`, data).then((r) => r.data), remove: (id: string) => apiClient.delete(`/${resource}/${id}`), }); // Usage: export const productsApi = createCrudApi<ProductDTO>("products"); ``` ### Request placement rule Two questions decide where a request function goes. Ask them in order. **Question 1: does one consumer own this request, or is it genuinely shared?** One consumer means the request stays with that consumer. This is the pages-first rule applied to `api/` segments. - Data fetching for a single page (e.g., dashboard stats) → `pages/<name>/api/` - An action owned by a single feature (e.g., `toggleLike`) → `features/<name>/api/` Genuinely shared between consumers: continue to question 2. **Question 2: does the request carry domain rules?** Domain rules are permission checks, status transitions, derived calculations, or a model the frontend composes from several responses. Knowing a resource's URL and response shape is not a domain rule. - No domain rules → `shared/api/`. Plain resource access is infrastructure no matter how many slices call it. Generic CRUD belongs here; build it from `shared/api/create-crud-api.ts`. - Domain rules → `entities/<name>/api/`, once the boundary is stable. A `getUserById` that only wraps `GET /users/:id` stays in `shared/api/` even when every page calls it. If it resolves the caller's permissions first, ask who owns that rule: an established user domain puts it in `entities/user/api/`, while a rule that only one screen applies stays with that page or feature. > **Where this comes from.** The official API requests guide defaults > request functions to `shared/api` or the consuming slice's `api` > segment, and warns against placing API calls in `entities` prematurely. > Question 2 follows that advice. Existing code placed under an earlier > reading is still not a violation: a request function already sitting in > `entities/<name>/api/` without domain rules keeps working. Relocate it > when you are already changing that slice, or when the entity has no > other reason to exist. Do not sweep a repository to move these > functions. -
cross-import-patterns.md 14.3 KB
# Cross-Import Resolution Patterns How to resolve cross-imports between slices on the same layer. Rule 4-3 disallows them by default, so the first move is always to remove the dependency by changing a boundary or a composition. Strategies A to C do that. Strategy D and `@x` are the documented exceptions for a dependency that cannot reasonably be removed, not a second way of working. Treat a cross-import as a code smell rather than an impossible state: some are deliberate, and those should be explicit and rare. ## What is a cross-import? A cross-import is an import between different slices within the same layer. For example: - importing `features/apply-coupon` from `features/add-to-cart` - importing `widgets/sidebar` from `widgets/header` The `shared` and `app` layers do not have slices, so imports within those layers are not cross-imports. ## Why is this a code smell? Cross-imports blur domain boundaries and introduce implicit dependencies. Four concrete problems: 1. **Unclear ownership and responsibility.** When `cart` imports from `product`, it becomes unclear which slice owns the shared logic. A change to `product`'s public contract now forces a change in `cart`, and a deep import couples `cart` to `product`'s internals as well. This makes bugs harder to localize and code harder to reason about. 2. **Reduced isolation and testability.** A core benefit of sliced architecture is that a slice can be read, changed, and tested with little knowledge of its siblings. Cross-imports break that. Testing `cart` now requires setting up `product`, and a change in one slice can fail tests in another. 3. **Increased cognitive load.** Working on `cart` now requires accounting for how `product` is structured. As cross-imports accumulate, tracing the impact of a change requires following more code across slice boundaries. 4. **Path to circular dependencies.** Cross-imports often start as one-way dependencies but evolve into bidirectional ones (A imports B, B imports A). This locks slices together and makes refactoring increasingly costly. ## Entities layer: prefer boundary merge over @x Cross-imports in `entities` are usually caused by splitting entities too granularly. Before reaching for `@x`, consider whether the boundaries should be merged instead. The `@x` notation is available as a dedicated cross-import surface for `entities`, but it should be treated as a **last resort**, a **necessary compromise**, not a recommended approach. Think of `@x` as an explicit gateway for unavoidable domain references, not a general-purpose reuse mechanism. Overuse locks entity boundaries together and makes refactoring more costly over time. ### How @x works (when boundary merge is genuinely impossible) Each entity exposes a special `@x/` directory containing files named after the consuming entity. This makes the cross-import explicit and auditable. **Direction rule:** in the path `entities/A/@x/B`, **A is the producer and B is the consumer**. Read it as "A crossed with B": the file `A/@x/B.ts` is the public API that A exposes specifically for B. So in the example below, `entities/user/@x/order.ts` is what `user` exposes to `order`, and `order` imports from it. ```text entities/ user/ @x/ order.ts ← Exposed specifically for the order entity model/ user.ts index.ts order/ model/ order-summary.ts ← Imports from user/@x/order index.ts ``` ```typescript // entities/user/@x/order.ts: exposes only what order needs export { getUserDisplayName } from "../model/user"; // entities/order/model/order-summary.ts import { getUserDisplayName } from "@/entities/user/@x/order"; ``` ### Rules when using @x 1. Document why `@x` is needed and why merging boundaries does not apply. 2. Review periodically. Requirements change and `@x` may become unnecessary. 3. Minimize the surface area of `@x` exports. 4. Only between entities. Features and widgets should use Strategy C or D below, not `@x`. ## Features and widgets: four strategies In `features` and `widgets`, multiple strategies are available depending on project context. Cross-imports here are not always forbidden; they are dependencies that should be deliberate. The four strategies below are listed in preferred order, but each fits different situations. ### Strategy A: slice merge If two slices are not truly independent and always change together, merge them into a single larger slice. ```text // Before: two features that always change together features/edit-profile/ features/edit-profile-privacy/ // After: one cohesive feature features/edit-profile/ ui/ EditProfileForm.tsx PrivacyFields.tsx model/ edit-profile.ts privacy.ts index.ts ``` If two slices keep cross-importing each other and effectively move as one unit, they are likely one feature in practice. Merging is often the simpler and cleaner choice. ### Strategy B: move a shared domain responsibility into an entity If multiple features share a domain rule or domain state, move that responsibility into the entity that owns it. Key principles: - What moves down is an established domain responsibility, not feature UI or user-flow orchestration. - Interaction-specific UI and workflow logic stay in `features`. - Features import that responsibility through the entity's public API. For example, if `features/add-to-cart` and `features/buy-now` both need the rule for whether a product can be purchased, that rule belongs to the product domain, while each button remains its own user action. ```text entities/ product/ model/ can-purchase.ts ← the shared domain rule index.ts features/ add-to-cart/ ui/AddToCartButton.tsx model/add-to-cart.ts ← imports canPurchase from @/entities/product index.ts buy-now/ ui/BuyNowButton.tsx model/buy-now.ts ← imports the same canPurchase index.ts ``` ### Strategy C: compose from an upper layer (IoC) When several lower-layer modules have to take part in one composition, assemble them in a layer above all of them. A page can import multiple features and entities to compose a screen, and components can be passed through props or children where that helps. By default one feature does not import another; Strategy D below is the documented exception when that dependency cannot be removed. Instead of connecting slices within the same layer via cross-imports, compose them at a higher level (`pages` or `app`). The upper layer assembles and connects the slices; the slices themselves do not know about each other. Common Inversion of Control techniques: - **Render props (React)**: pass components or render functions as props. - **Slots (Vue)**: use named slots to inject content from parent components. - **Dependency injection**: pass dependencies through props or context. #### Basic composition (React) ```typescript // features/follow-user/index.ts export { FollowButton } from "./ui/FollowButton"; // features/report-user/index.ts export { ReportUserButton } from "./ui/ReportUserButton"; // pages/profile/ui/ProfilePage.tsx import { FollowButton } from "@/features/follow-user"; import { ReportUserButton } from "@/features/report-user"; export const ProfilePage = ({ userId }) => ( <div> <FollowButton userId={userId} /> <ReportUserButton userId={userId} /> </div> ); ``` Following and reporting are two user actions that happen to sit on one screen. Neither knows the other exists; the page puts them there. #### Render props (React) When one feature's UI has to place another feature's control inside it, use a render prop to invert the dependency: ```typescript // features/manage-wishlist/ui/WishlistItems.tsx // RemoveButton is this feature's own ui/, not a cross-import. interface WishlistItemsProps { items: WishlistItem[]; renderAddToCart?: (productId: string) => React.ReactNode; } export const WishlistItems = ({ items, renderAddToCart }: WishlistItemsProps) => ( <ul> {items.map((item) => ( <li key={item.id}> <span>{item.title}</span> <RemoveButton itemId={item.id} /> {renderAddToCart?.(item.productId)} </li> ))} </ul> ); // pages/wishlist/ui/WishlistPage.tsx import { WishlistItems } from "@/features/manage-wishlist"; import { AddToCartButton } from "@/features/add-to-cart"; export const WishlistPage = () => ( <WishlistItems items={items} renderAddToCart={(productId) => <AddToCartButton productId={productId} />} /> ); ``` `manage-wishlist` never imports `add-to-cart`. It leaves a hole per row, and the page fills it. #### Slots (Vue) Vue's slot system provides a natural way to compose features without cross-imports: ```vue <!-- features/manage-wishlist/ui/WishlistItems.vue --> <script setup lang="ts"> defineProps<{ items: WishlistItem[] }>(); </script> <template> <ul> <li v-for="item in items" :key="item.id"> <span>{{ item.title }}</span> <slot name="add-to-cart" :productId="item.productId" /> </li> </ul> </template> <!-- pages/wishlist/ui/WishlistPage.vue --> <script setup lang="ts"> import { WishlistItems } from "@/features/manage-wishlist"; import { AddToCartButton } from "@/features/add-to-cart"; </script> <template> <WishlistItems :items="items"> <template #add-to-cart="{ productId }"> <AddToCartButton :productId="productId" /> </template> </WishlistItems> </template> ``` ### Strategy D: cross-feature reuse only via Public API If strategies A through C do not fit and cross-feature reuse is genuinely unavoidable, allow it only through an explicit Public API (exported hooks or UI components). Do not access another slice's `store`, `model`, or internal implementation. Unlike strategies A through C, which aim to eliminate cross-imports, this strategy accepts them while minimizing risk through strict boundaries. ```typescript // features/auth/index.ts export { useAuth } from "./model/use-auth"; export { AuthButton } from "./ui/AuthButton"; // features/edit-profile/ui/ProfileMenu.tsx import { useAuth, AuthButton } from "@/features/auth"; export const ProfileMenu = () => { const { user } = useAuth(); if (!user) return <AuthButton />; return <div>{user.name}</div>; }; ``` The boundary holds: `features/edit-profile` cannot import from `@/features/auth/model/internal/*`. Only what `features/auth` explicitly exposes through `index.ts` is reachable. The `@x` notation is for the entities layer only. Features and widgets use strategies A through D above; their access path is the standard public API (`index.ts`), not a dedicated cross-import surface. ## When to treat a cross-import as a problem After reviewing these strategies, the question is: when is a cross-import acceptable to keep, and when should it be treated as a code smell and refactored? Common warning signs: - Directly depending on another slice's `store`, `model`, or business logic - Deep imports into another slice's internal files (bypassing the public API) - Bidirectional dependencies (A imports B, and B imports A) - Changes in one slice frequently breaking another slice - Flows that should be composed in `pages` or `app`, but are forced into cross-imports within the same layer When these signals appear, treat the cross-import as a code smell and apply one of the strategies above. ## Strictness depends on project context The strictness of cross-import enforcement depends on the project: - In **early-stage products** with heavy experimentation, allowing some cross-imports may be a pragmatic speed trade-off. - In **long-lived or regulated systems** (fintech, large-scale services), stricter boundaries pay off in maintainability and stability. Cross-imports are not an absolute prohibition. They are dependencies that are generally best avoided, but sometimes used intentionally. If a cross-import is introduced: - Treat it as a deliberate architectural choice. - Document the reasoning in code (a comment explaining why other strategies do not apply). - Revisit it periodically as the system evolves; if requirements change, the cross-import may no longer be needed. ## Decision flow for AI agents ```text Two slices on the same layer need to share code. │ ├─ ENTITIES layer? │ ├─ Are they one cohesive domain boundary? │ │ └─ YES → Merge. Stop. │ ├─ Does either entity really need to know the other, or can a │ │ page or feature hold both? │ │ └─ Compose above them. Stop. │ ├─ Is the shared part business-neutral infrastructure? │ │ └─ YES → Move that part to shared/. Stop. │ └─ Boundaries must stay separate and the domain dependency is real? │ └─ Use @x as last resort. Document why merge is not possible. │ └─ FEATURES or WIDGETS layer? ├─ Strategy A: Do they always change together? │ └─ YES → Merge slices. │ ├─ Strategy B: Is the shared part domain-only logic? │ └─ YES → Push down to entities. Keep UI in features. │ ├─ Strategy C: Can the connection be assembled by a higher layer? │ └─ YES → Compose in pages or app via render props, slots, or DI. │ └─ Strategy D: Is reuse genuinely unavoidable and the access surface limited to a Public API? └─ YES → Allow, but only through index.ts. Never reach into model/, store/, or internal files. Do not use @x in features or widgets. ``` ## Anti-patterns - **Reaching for `@x` in features or widgets.** `@x` is for entities only. Use Strategy C (compose) or D (Public API) instead. - **Treating `@x` as a clean solution.** It is a compromise. If you find yourself adding multiple `@x` files between the same entities, the boundaries are probably wrong. Merge them. - **Bypassing the Public API to access internals.** Even when Strategy D is in use, importing from `@/features/auth/model/internal/*` defeats the purpose. Restrict yourself to what `index.ts` exports. - **Bidirectional cross-imports.** A imports B and B imports A says the boundaries or the composition are wrong. Re-check whether the slices are one boundary, whether the shared part belongs lower, or whether the composition should move up. ## See also - `references/excessive-entities.md`: prevent the conditions that lead to entity-layer cross-imports in the first place. - `references/layer-structure.md`: layer rules and import directions. -
excessive-entities.md 8.8 KB
# Excessive Entities How to keep the `entities` layer clean and avoid over-extracting business logic into entities. Excessive entities cause ambiguity (what code belongs where), coupling, and constant import dilemmas as code scatters across sibling entities. An entity is not where every reusable business concept goes. It is a stable, low-level domain boundary that several consumers genuinely have to share. ## Why this matters The `entities` layer is one of the lower layers and is widely accessible. Every layer except `shared` can import from it. That global nature means changes to `entities` propagate widely, so the boundaries need care up front to avoid costly refactors. Adding an entity is cheap; removing one after many consumers depend on it is expensive. ## How to keep entities clean ### 0. Consider having no entities layer An FSD application without an `entities` layer is still FSD. Skipping the layer simplifies the architecture and keeps it available for future scaling. **Thin clients** (where the backend handles most data processing and the client mostly exchanges data) usually do not need an entities layer. **Thick clients** (significant client-side business logic) are better candidates for entities. The classification is not strictly binary. Different parts of the same application may behave as thick or thin clients. ```text // Thin client without entities layer (still valid FSD) src/ app/ pages/ dashboard/ profile/ shared/ api/ ui/ ``` ### 1. Avoid preemptive slicing FSD v2.1 encourages **deferred decomposition** of slices. Place code in the `model` segment of the consuming page (widget, feature) first. Move it to `entities` later, when business requirements stabilize and reuse is confirmed across multiple consumers. The later code moves to `entities`, the less dangerous the refactor. Code in `entities` can affect every higher-layer slice that imports it. ```text // Iteration 1: code lives where it is used pages/profile/ model/ profile-validation.ts ← page-specific for now // Iteration 2 (once several pages must share one copy of the rule): entities/profile/ model/ profile-validation.ts ← moved once the rule needs one home ``` ### 2. Avoid unnecessary entities Do not create an entity for every piece of business logic. Use types from `shared/api` and place logic in the `model` segment of the current slice. For genuinely reusable business logic, use the `model` segment within an entity slice while the transport types stay in `shared/api`. That means the API shapes, `OrderDto` here; a type that exists because of a business rule can belong to the entity model once the entity does. ```text shared/ api/ endpoints/ order.ts ← OrderDto type and request functions entities/ order/ model/ apply-discount.ts ← Business logic that uses OrderDto index.ts ``` The DTO lives in `shared/api/endpoints/order.ts`. Once an `order` boundary has been earned, reusable rules that operate on it (calculating discounts, applying promotions) live in `entities/order/model/`. Until then they stay in the consuming slice's `model/`. Do not mirror every API endpoint with a corresponding entity. ### 3. Exclude CRUD operations from entities CRUD operations involve boilerplate code without significant business logic. Putting them in `entities` clutters the layer and obscures the code that genuinely matters. Where it goes instead is the request placement rule's answer, not a fixed path: a single consumer keeps it in that slice's `api/`, and plain resource access shared across consumers lands in `shared/api` (`references/auth-and-api.md`): ```text shared/ api/ client.ts endpoints/ order.ts ← getOrder, createOrder, updateOrder, deleteOrder products.ts ← Standard CRUD for products cart.ts ← Standard CRUD for cart index.ts ``` For complex CRUD with atomic updates, rollbacks, or transactions, evaluate whether the operation carries business rules. Complexity alone does not send it to `entities`: decide by what owns the rule. A checkout or a cancellation is a use case and belongs to a feature or the page that runs it; a rule the domain owns belongs to the entity; anything else stays in `shared/api`. ### 4. Store authentication data in shared Prefer `shared/auth` (or `shared/api`) over a `user` entity for tokens and session DTOs. They are specific to authentication, rarely reused outside it, and wrapping a login response in a `user` entity tends to pull the entity into `@x` chains. A `user` entity earns its place when user-domain responsibilities hold a stable boundary outside the login flow, such as profile identity read across several product contexts (avatars in comments, names in posts). An entities layer that already exists is not itself a reason, and neither is profile reuse on its own. Tokens and the session stay in `shared/auth` unless an established entity genuinely owns that state. Both folder shapes, when to split `shared/auth` from `shared/api`, and the three ways to expose the token to the API client are in `references/auth-and-api.md`. ### 5. Minimize cross-imports FSD permits cross-imports between entities via `@x`, but they introduce technical issues including circular dependencies. Design entities within **isolated business contexts** so cross-imports become unnecessary. **Non-isolated context (avoid):** ```text entities/ order/ @x/ model/ order-item/ @x/ model/ order-customer-info/ @x/ model/ ``` Three sibling entities all referencing each other through `@x`. This is a sign that the boundaries are wrong. **Isolated context (preferred):** ```text entities/ order-info/ model/ order-info.ts ← order, items, and customer info together index.ts ``` One entity encapsulates the related logic, so there is no `@x` file and no way for the sibling slices to form a cycle. The general rule: when several entities have `@x` dependencies on each other, treat that as a signal to merge the boundaries, not as something to manage. ## Decision tree for AI agents ```text A new piece of domain-related code or state needs a home. │ ├─ Is the project a thin client? │ └─ YES → Strong signal to start without entities. Keep reading │ the branches below rather than stopping here. │ ├─ Is the logic used in only one place right now? │ └─ YES → Keep in the consuming slice's model/. Defer extraction. │ ├─ Is it a CRUD operation without business meaning? │ ├─ One consumer → that slice's api/ segment │ └─ Shared across consumers → shared/api/endpoints/<resource>.ts │ ├─ Is it auth data (tokens, session, login DTOs)? │ ├─ Does an established user or session entity already own this │ │ state, rather than merely existing? │ │ └─ YES → that entity's model/ │ └─ Otherwise → shared/auth/ (the default). │ An entities layer existing is not a reason to move it. │ Avoid placing in a page, widget, or single feature slice. │ ├─ Is it just a TypeScript type for an API response? │ └─ YES → shared/api/. No entity needed for types alone. │ └─ Is it reused now, stable enough to name, and required to stay consistent across its consumers? └─ YES → Create entities/<name>/model/. Verify the boundary is isolated and does not require @x to communicate with sibling entities. ``` ## Anti-patterns - **Creating entities preemptively.** Wait for reuse that is real, has a reason to change of its own, and needs one authoritative copy. - **Mirroring every API endpoint with an entity.** API endpoints belong in `shared/api`. Entities exist for business logic, not for paralleling the backend structure. - **Creating a `user` entity *only* to wrap a login response.** A `user` entity is justified when a stable user-domain responsibility is shared across non-auth flows (avatars in comments, names in posts). Until then `shared/auth` is simpler. The official Auth guide accepts a token store in Shared or in an entities slice for the current user or session (`references/auth-and-api.md`); what it rules out is a page, widget, or single feature slice. - **Splitting one domain into many entities (`order`, `order-item`, `order-customer-info`).** This produces `@x` chains. Merge into a single isolated context (`order-info` or `order`). - **Putting CRUD wrappers in entities.** They clutter the layer. Place them by the request placement rule: with their consumer while there is one, in `shared/api/endpoints/` once several slices call them. ## See also - `references/cross-import-patterns.md`: how to handle cross-imports when they appear, and why `@x` is a last resort. - `references/layer-structure.md`: layer responsibilities and the entities segment shape. -
framework-integration.md 15.7 KB
# Framework Integration How to set up FSD within specific frameworks. Covers directory placement, routing integration, and framework-specific path alias configuration. ## General principle Place FSD layers inside `src/` to avoid naming conflicts with framework directories. The FSD `app/` and `pages/` layers are **not** the same as framework directories with the same names (e.g., Next.js `app/`). Examples here import through `@/` pointing at `src/`. Whether that is one root alias or one entry per layer is a tooling choice, not an FSD rule, and it changes no layer semantics; how the resolver is wired differs by framework, which is what each section below shows. Configure an entry only for a layer the project actually has. The FSD layers inside `src/` keep the standard shape described in `references/layer-structure.md`. Each section below shows only what the framework adds or renames around them, not the layers' own internals. The Next.js, Nuxt, and Astro sections follow the official tech guides on fsd.how. React Router and Vite are this skill's own additions with no official guide behind them. SvelteKit and Electron have official guides and are not covered here; read those directly. ## Next.js FSD works with both the App Router and the Pages Router. Next.js uses the `app/` and `pages/` folder names for its own routing. Those names collide with the FSD `app/` and `pages/` layers. Rename the FSD layers to `_app/` and `_pages/` (with the underscore prefix). Do this even if you only use one router. Keep the Next.js routing folders at the project root so `src/` holds only FSD code. The FSD linter (Steiger) expects this naming. ### Projects on the previously recommended pattern An earlier version of this guide recommended a different layout. It kept the Next.js `app`/`pages` folders at the root and added an empty root `pages/` placeholder. The `src/app`/`src/pages` layers were not prefixed. Projects set up that way keep working. The empty `pages/` placeholder can break the build on Next.js 13.5 and later. That is why the prefix is now the default. Use `_app`/`_pages` for new projects. Move a project off the old pattern when you can. ### App Router Route files in `app/` re-export from the FSD `_pages/` layer. #### Directory structure ```text my-nextjs-project/ app/ ← Next.js App Router (routing only) layout.tsx page.tsx profile/ page.tsx api/ get-example/ route.ts src/ _app/ ← FSD app layer providers/ index.tsx ← All providers (QueryClient, theme, etc.) styles/ globals.css api-routes/ ← Route Handler implementations (see below) index.ts get-example-data.ts _pages/ ← FSD pages layer home/ ← slice; segments per layer-structure.md profile/ widgets/ ← FSD widgets layer (when needed) features/ ← FSD features layer (when needed) entities/ ← FSD entities layer (when needed) shared/ ← FSD shared layer db/ ← Database queries (see below) ``` #### Wiring Next.js routes to FSD pages ```typescript // app/layout.tsx import { Providers } from '@/_app/providers'; import '@/_app/styles/globals.css'; export default function RootLayout({ children }) { return ( <html lang="en"> <body><Providers>{children}</Providers></body> </html> ); } // app/example/page.tsx: re-export the FSD page (component + metadata) export { ExamplePage as default, metadata } from '@/_pages/example'; ``` Keep route files free of logic. Re-export the page component plus whatever route exports the framework needs and the FSD page provides, such as `metadata` or `generateMetadata` when the page has one. ### Pages Router The Pages Router uses `pages/` at the project root. Each route file should re-export the corresponding page module from the FSD `_pages/` layer. ```text my-nextjs-project/ pages/ ← Next.js Pages Router (routing only) _app.tsx api/example.ts ← API route re-export example/index.tsx src/ _app/ custom-app/ ← Custom App component api-routes/ ← Route Handler implementations _pages/ example/ ui/example.tsx index.ts ``` ```typescript // pages/example/index.tsx export { Example as default } from '@/_pages/example'; // pages/_app.tsx: re-export the custom App from src/_app/custom-app export { App as default } from '@/_app/custom-app'; ``` The custom App implementation lives in `src/_app/custom-app/` and exposes only what the framework entry file imports. App has no slices, so treat this as a segment boundary inside the layer, not a slice. ### Proxy and instrumentation `proxy.ts` and `instrumentation.ts` sit at the framework boundary, not inside the FSD layers. Next.js looks for them in the project root, or in `src/` when the Next.js app itself uses `src/`, at the same level as its `app/` or `pages/` folder. Keep them there and out of `src/_app/`. Next.js 16 deprecated the `middleware` convention and renamed it `proxy`; older projects still use `middleware.ts`. With the layout above, the Next.js `app/` and `pages/` folders sit at the project root, so both files sit there too. The `src/` option applies to projects that keep Next.js routing under `src/`, and never means `src/_app/`. > **Where this comes from.** The official Next.js guide on fsd.how says > both files must be in the project root and that Next.js will not find > them under `src/`. That matched earlier Next.js versions and is no > longer what the framework documents, so this section follows the > framework. ### Route Handlers (API routes) Use a dedicated `api-routes` segment in the FSD `_app/` layer (`src/_app/api-routes/`) to host the actual request handlers. The Next.js `app/api/*/route.ts` (App Router) or `pages/api/*.ts` (Pages Router) files become thin re-exports. **App Router:** ```typescript // src/_app/api-routes/get-example-data.ts import { getExamplesList } from '@/shared/db'; export const getExampleData = () => { try { const examplesList = getExamplesList(); return Response.json({ examplesList }); } catch { return Response.json(null, { status: 500, statusText: 'Ouch, something went wrong', }); } }; // src/_app/api-routes/index.ts export { getExampleData } from './get-example-data'; // app/api/example/route.ts export { getExampleData as GET } from '@/_app/api-routes'; ``` **Pages Router:** ```typescript // src/_app/api-routes/get-example-data.ts import type { NextApiRequest, NextApiResponse } from 'next'; const config = { api: { bodyParser: { sizeLimit: '1mb' } }, maxDuration: 5 }; const handler = (req: NextApiRequest, res: NextApiResponse) => res.status(200).json({ message: 'Hello from FSD' }); export const getExampleData = { config, handler } as const; // pages/api/example.ts import { getExampleData } from '@/_app/api-routes'; export const config = getExampleData.config; export default getExampleData.handler; ``` Keep Route Handlers as framework-facing adapters and delegate domain rules to the FSD boundary that owns them. FSD is primarily a frontend methodology. If `api-routes` grows to many endpoints, consider moving the backend to a separate package in a monorepo. ### Database access Place database queries in a `db` segment in `shared/` (`src/shared/db/`). Co-locate caching and revalidation logic with the queries themselves. Plain data access is all that goes there. Rule 4-5 does not relax because code runs on the server: domain rules and use-case orchestration stay with the slice that owns them. ### Path aliases ```json // tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@/_app/*": ["src/_app/*"], "@/_pages/*": ["src/_pages/*"], "@/shared/*": ["src/shared/*"] } } } ``` Next.js reads `tsconfig.json` paths automatically. No `next.config.js` alias configuration is needed. ### Server and client public APIs In the Next.js App Router, a single slice can contain both client-usable modules and server-only modules. Keep `index.ts` free of server-only exports, such as Server Components or data-access functions that import `server-only`. When a Client Component imports the slice, those exports can enter the client module graph and cause build errors. Split only when this boundary is required. Put server-only exports in `index.server.ts`. ## Nuxt (v3-compatible layout) Nuxt keeps file routing in `pages/` at the project root, the name FSD reserves for the pages layer. The official Nuxt guide resolves this by moving Nuxt's routing folder inside the FSD `app` layer and giving `src/` a single `@` alias. This section mirrors that guide. The shape below is a Nuxt 3 project, which is what the official guide covers. Nuxt 4 changed the default `srcDir` to `app/` and moves `pages/`, `layouts/`, and `components/` under it, so its framework `app/` collides with the FSD app layer the way Next.js does. No official FSD guide covers that layout yet. A Nuxt 4 project can keep this shape, which Nuxt still auto-detects, and follow this section. Nuxt 3 itself reached end of life on 31 July 2026, so read the heading as the layout, not the version to start on. ### Directory structure ```text my-nuxt-project/ nuxt.config.ts src/ app/ ← FSD app layer routes/ ← Nuxt file routing (dir.pages) index.vue layouts/ ← Nuxt layouts (dir.layouts) pages/ ← FSD pages layer home/ ui/home-page.vue index.ts shared/ ``` ### nuxt.config.ts ```typescript // nuxt.config.ts export default defineNuxtConfig({ alias: { "@": "../src", }, dir: { pages: "./src/app/routes", layouts: "./src/app/layouts", }, }); ``` ### Wiring Nuxt routes to FSD pages ```vue <!-- src/app/routes/index.vue --> <script setup> import { HomePage } from "@/pages/home"; </script> <template> <HomePage /> </template> ``` The official guide also shows config-based routing through `app/router.options.ts` instead of file routing. Either way, route definitions live in the `app` layer and page slices in `pages`. ## Vite + React ### Directory structure ```text my-vite-project/ src/ app/ ← FSD app layer providers/ router.tsx styles/ main.tsx ← Entry point pages/ shared/ index.html vite.config.ts tsconfig.json ``` ### Path aliases Mirror the standard `tsconfig.json` mapping in `vite.config.ts` so the Vite resolver agrees with TypeScript: ```typescript // vite.config.ts import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import { resolve } from "path"; export default defineConfig({ plugins: [react()], resolve: { alias: { "@/app": resolve(__dirname, "src/app"), "@/pages": resolve(__dirname, "src/pages"), "@/shared": resolve(__dirname, "src/shared"), }, }, }); ``` Create React App is no longer maintained. A project still on it follows this section; the only difference is that the aliases go into a `craco` config instead of `vite.config.ts`. Migrate to Vite when you can. ## React Router (framework mode) React Router v7 in framework mode owns an `app/` directory for its root layout, route config, and route modules. That name collides with the FSD app layer, so apply the same fix as for Next.js: React Router's `app/` stays at the project root, FSD lives in `src/`, and the FSD app layer is renamed to `_app/`. Only `app/` collides, so `pages/` keeps its name. Library mode (`createBrowserRouter` inside a plain Vite app) has no framework directory. The Vite + React section applies as is, with the router defined in the FSD app layer. ### Directory structure ```text my-router-project/ app/ ← React Router (routing only) root.tsx ← Root layout, mounts providers from @/_app routes.ts ← Route config routes/ home.tsx ← Thin wrapper around @/pages/home product.tsx src/ _app/ ← FSD app layer providers/ styles/ pages/ home/ product/ ui/ProductPage.tsx api/fetch-product.ts index.ts shared/ react-router.config.ts vite.config.ts tsconfig.json ``` ### Wiring routes to FSD pages Route modules stay thin. They own the framework-required route exports and delegate the application work to what the FSD page's public API exposes. ```typescript // app/routes.ts import { type RouteConfig, index, route } from "@react-router/dev/routes"; export default [ index("routes/home.tsx"), route("products/:id", "routes/product.tsx"), ] satisfies RouteConfig; ``` ```typescript // app/routes/product.tsx import type { Route } from "./+types/product"; import { ProductPage, fetchProduct } from "@/pages/product"; export const loader = ({ params }: Route.LoaderArgs) => fetchProduct(params.id); export default function ProductRoute({ loaderData }: Route.ComponentProps) { return <ProductPage product={loaderData} />; } ``` **Framework requirement:** `loader` and the default component are Route Module exports, so both live in the route file, and React Router hands the component its generated `Route.ComponentProps`. **This skill's recommendation:** let the generated types stop there. The wrapper makes one call into the page's `api` segment or `shared/api` and passes plain props down, so the FSD page stays framework-independent. ### Path aliases Keep the standard `tsconfig.json` mapping, with `@/_app` pointing at `src/_app`, and let `vite-tsconfig-paths` feed it to the React Router Vite plugin: ```typescript // vite.config.ts import { defineConfig } from "vite"; import { reactRouter } from "@react-router/dev/vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [reactRouter(), tsconfigPaths()], }); ``` ## Astro Astro uses `src/pages/` for file-based routing, which collides with the FSD `pages/` layer. Move the FSD pages layer to `src/_pages/` (with the underscore prefix) and reserve `src/pages/` for Astro routes. ### Directory structure ```text my-astro-project/ src/ pages/ ← Astro routing (thin entry points) 404.astro index.astro _pages/ ← FSD pages layer home/ ui/HomePage.astro index.ts features/ ← when needed entities/ ← when needed widgets/ ← existing projects that keep the layer shared/ ``` ### Wiring Astro routes to FSD pages The Astro route file imports and renders the FSD page, nothing else: ```astro --- // src/pages/index.astro import { HomePage } from '@/_pages/home'; --- <HomePage /> ``` ### Path aliases (tsconfig.json) The official FSD Astro guide uses a single `@/*` alias pointing at `src/*` rather than one alias per layer, and this section follows it: ```json { "extends": "astro/tsconfigs/strict", "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } ``` Imports then reference the layer path directly: `@/_pages/home`, `@/shared/ui`, `@/entities/user`. ### Working with integrations Some Astro integrations (for example, Starlight) use content collections that expect content in fixed folders such as `src/content/docs/`. If the integration does not allow the path to be changed, leave it as-is. The content folder lives alongside FSD layers without collision: ```text src/ _pages/ ← FSD pages layer content/ ← Integration content (Starlight, etc.) docs/ getting-started.md shared/ ← FSD shared layer ``` Let the integration handle its own routing and rendering, while FSD layers manage application-specific code. -
growth-walkthrough.md 7.9 KB
# Growth Walkthrough One small shop through four snapshots, showing which moments earn a layer and which do not. Read it when starting a project, or when deciding whether entities are needed yet. Each snapshot gives the tree, what changed in the product, and which rule from `SKILL.md` decided the response. Every decision here comes from `SKILL.md`; this file adds no placement rules of its own. Not every product change earns a layer: a layer is earned by a stable responsibility that needs one home, not by a count of how many places use something. ## Snapshot 0: two pages, three layers The shop has a home page with a product list and a product detail page. The detail page shows an "on sale" badge. ```text src/ app/ providers/ router.tsx styles/ pages/ home/ ui/HomePage.tsx ui/ProductCard.tsx ← list card, used only here api/fetch-products.ts ← one consumer: this page index.ts product/ ui/ProductPage.tsx ui/SaleBadge.tsx ← badge, used only here model/is-on-sale.ts ← the rule: price < listPrice api/fetch-product.ts ← one consumer: this page index.ts shared/ api/ client.ts product.ts ← ProductDTO, read by both pages index.ts ui/ Button/ Card/ ``` **What is absent on purpose.** No `entities/`, no `features/`, no `widgets/`. Each request has one consumer, so it sits in that page's `api/` segment: Question 1 of the request placement rule is asked before Question 2 (`references/auth-and-api.md`). `ProductDTO` is in `shared/api` because both pages read the same transport shape. The sale rule sits in the product page because only that page applies it (Step 1). This is complete, valid FSD (Section 5-3). ## Snapshot 1: a third page reuses product data, no layer appears A search page is added. It fetches products, shows them as cards, and marks the ones on sale. ```text pages/ home/ index.ts ← api/fetch-products.ts moved out search/ ← new slice ui/SearchPage.tsx ui/ProductCard.tsx ← a second card, copied from home model/is-on-sale.ts ← a second copy of the rule, from product index.ts shared/ api/ product.ts ← ProductDTO, and now fetchProducts ``` **The request moved down, and no layer opened.** `fetchProducts` has two consumers now, so Question 1 stops keeping it in the home page. Question 2 asks whether it carries domain rules, and a URL with a response shape is not a rule, so it lands in `shared/api` next to the DTO. Note where it did not land: `entities/product/api` was never a candidate, and the official API requests guide warns against putting requests there prematurely. Moving code down a layer is not the same as opening one. `fetchProduct` did not move. The detail page is still its only consumer. **The second card is a copy, not an extraction.** Two `ProductCard` files look like a signal for `entities/product/ui`. They are not, yet. The search card shows a match snippet and the home card does not, so they are not the same code, and they will keep drifting apart for their own reasons. Step 1 covers this case directly: used in two pages but the duplication is manageable, so separate copies are valid. Extracting now would force two cards that want to differ into one component that has to serve both. **The rule is copied too.** The search card marks sale items, so `is-on-sale.ts` is copied out of the product page. Keeping both copies local is still cheaper than committing to a shared boundary before the two consumers are required to agree, and so far nothing requires it. A second copy is not a boundary on its own. What turns one into a boundary is the subject of Snapshot 2. ## Snapshot 2: a rule diverges, `entities/product` appears Marketing changes what "on sale" means: the price must be below the list price *and* the item must be in stock. The product page is updated. The copy in the search page, made in Snapshot 1, is not. Search now marks items on sale that the detail page says are not. This is the signal. The two copies are the same rule, they must agree, and they no longer do. Check the extraction rule (`SKILL.md`, Section 1): 1. The same code is used in multiple places right now. Yes, two pages. 2. It has a reason to change that is independent of any one consumer. Yes: the rule changes when marketing changes it, not when either page changes. 3. The boundary has a focused responsibility. Yes: "is this product on sale" and nothing else. All three hold, so the rule gets one home (Step 4). ```text entities/ ← new layer product/ model/is-on-sale.ts ← replaces both copies; the one home index.ts pages/ product/ ui/SaleBadge.tsx ← stays; now calls isOnSale from @/entities/product search/ ui/ProductCard.tsx ← stays; calls the same isOnSale shared/ api/ product.ts ← ProductDTO stays here ``` **What did not move.** The transport type `ProductDTO` stays in `shared/api`. The official excessive-entities guide moves the logic into the entity's `model` and leaves the API shape where it was (Section 5-2, item 3). A transport type does not follow business logic into an entity just because that logic reads it. The badge and the cards stay in their pages: they are UI, and Section 6 warns against adding UI to entities until there is a reason. The entity is one file and an index. That is enough. ## Snapshot 3: an action is reused, `features/add-to-cart` appears Between Snapshots 2 and 3 the product page gains an "Add to cart" button, with its request and an optimistic cart update in the page's `api` and `model` segments. Search results now need the same action. Step 3 asks whether this is a complete user action, used in multiple places, with a stable boundary. The button, the request, and the cart update form one action; two pages use it; adding to the cart means the same thing from either page. Extract it. The request goes with the feature because it is the add-to-cart use case itself, not a generic cart CRUD wrapper. A plain reusable cart request would have stayed in `shared/api` however many slices called it. ```text features/ ← new layer add-to-cart/ ui/AddToCartButton.tsx api/add-to-cart.ts ← the use case itself, not cart CRUD model/add-to-cart.ts ← pending and optimistic state for it index.ts pages/ product/ ui/ProductPage.tsx ← renders <AddToCartButton /> search/ ui/ProductCard.tsx ← renders <AddToCartButton /> ``` **What did not appear.** A `cart` entity. The state here exists only to run the add-to-cart interaction and carries no cart-domain responsibility anyone else could reuse, so Step 4 keeps it inside the feature. A cart a checkout page and an order summary both had to agree on would be a different question. A `widgets/` layer. Nothing in four snapshots needed one, and the callout in Section 1 says not to reach for it. ## What the walkthrough shows | Moment | Trigger | Response | Rule | | --- | --- | --- | --- | | 0 | Two pages | `app/`, `pages/`, `shared/` | Section 5-3 | | 1 | Third page reads product data | No layer; `fetchProducts` moves to `shared/api` | Question 1, Question 2 | | 2 | Same rule, two copies, one stale | `entities/product/model` | The extraction rule, Step 4 | | 3 | Same complete action on two pages | `features/add-to-cart` | Step 3 | Reuse alone opened no layer. A domain rule that had to stay consistent across its consumers earned `entities`; a complete user action with one shared behavior earned `features`. Everything else stayed where it was used, and one request moved down without earning anything. Once a layer exists, `references/layer-structure.md` shows the full shape of its slices and segments. This file only shows the moment it appears. -
layer-structure.md 19.3 KB
# Layer Structure Reference Detailed folder structures, code examples, and naming conventions for each FSD layer. Use this reference when creating, reviewing, or reorganizing project structure. ## App layer App-wide initialization: providers, routing, global styles, entry point. Organized by segments only, no slices. The methodology does not formally standardize App segment names. The common convention list (`ui`, `api`, `model`, `lib`, `config`) applies to all layers but is rarely a good fit here. In practice, projects use names that describe purpose: `routes`, `store`, `styles`, `providers`, `entrypoint`, etc. Choose names that match your stack (for example, `providers` for React/Vue provider components that wrap Redux, QueryClient, or theme contexts): ```text app/ routes/ ← Route configuration (or router.tsx for single file) store/ ← Global state store (Redux configureStore, Zustand root) styles/ ← Global CSS, reset, theme variables providers/ ← Provider components (Redux Provider, QueryClientProvider) entrypoint.tsx ← Application entry point (main.tsx, index.tsx) ``` A smaller project may collapse some of these into single files: ```text app/ router.tsx ← Route configuration store.ts ← Store configuration styles/ global.css providers.tsx ← All providers in one file index.tsx ← Entry point ``` ```typescript // app/router.tsx import { HomePage } from '@/pages/home'; import { ProfilePage } from '@/pages/profile'; export const router = createBrowserRouter([ { path: '/', element: <HomePage /> }, { path: '/profile/:id', element: <ProfilePage /> }, ]); ``` **Belongs in app:** Global providers (Redux store, QueryClient, theme), routing setup, global styles, error boundaries, analytics initialization. **Does not belong:** Feature-specific code, business logic, page-level UI. ## Pages layer Route-level composition. In v2.1, pages **own substantial logic**: they are not thin wrappers. In early project stages, most code lives here. ```text pages/ home/ ui/ HomePage.tsx HeroSection.tsx FeaturesGrid.tsx model/ home-data.ts ← Page-specific state + logic api/ fetch-home-data.ts ← Page-specific API calls index.ts profile/ ui/ ProfilePage.tsx ProfileForm.tsx ProfileStats.tsx model/ profile.ts ← Profile state + validation logic api/ update-profile.ts fetch-profile.ts index.ts ``` **Belongs in pages:** Page-specific UI, forms, validation, data fetching, state management, business logic, API integrations. Even code that looks reusable stays here if it is simpler to keep local. **Does not belong:** Code that is already reused across multiple pages, has a stable focused responsibility, and must have one shared home. Extract when all three hold, not when reuse alone appears. ### Page layout patterns A typical page composes features and entities from lower layers, plus its own local UI components: ```typescript // pages/product-detail/ui/ProductDetailPage.tsx import { AddToCart } from '@/features/add-to-cart'; import { ProductCard } from '@/entities/product'; import { PageHeader } from './PageHeader'; // local to this page export const ProductDetailPage = ({ productId }) => { const product = useProductDetail(productId); // local hook in this page return ( <> <PageHeader /> <ProductCard data={product} /> <AddToCart productId={productId} /> <RelatedProducts products={product.related} /> {/* local component */} </> ); }; ``` For pages that only need shared + page-local code (no extracted layers): ```typescript // pages/about/ui/AboutPage.tsx import { Card } from '@/shared/ui/Card'; import { TeamSection } from './TeamSection'; // local to this page import { MissionStatement } from './MissionStatement'; export const AboutPage = () => ( <main> <MissionStatement /> <Card><TeamSection /></Card> </main> ); ``` ## Widgets layer (discouraged) Widgets are a layer for placing reusable UI blocks. They can be composed from multiple UI elements into a meaningful section of a screen and then used in upper layers such as Pages or App. > **The official layer reference discourages using the Widgets layer**, and > this skill follows it. The reasoning, and where each kind of UI block > goes instead, is in `SKILL.md`, Section 1. One edge case: multiple flows from the Features layer may need to be composed together, the kind of case that previously would have been placed in Widgets. In most cases this can be resolved by taking a different approach to composition. The parent (`pages` or `app`) imports the features and connects them, which is Strategy C in `references/cross-import-patterns.md`. Still, there may be edge cases that are genuinely hard to resolve. When that happens, document the situation in [feature-sliced/skills#7](https://github.com/feature-sliced/skills/issues/7). Discouraging the layer does not mean removing it entirely. It means recommending against actively adopting it. Projects already using widgets can keep using them as before, and the standard slice/segment and public API rules apply just as on any other layer: ```text widgets/ header/ ui/ Header.tsx Navigation.tsx UserMenu.tsx model/ header.ts ← Widget state api/ fetch-notifications.ts index.ts ``` **If you still use widgets:** Navigation bars, sidebars, dashboards, and footers are the typical examples. Simple UI primitives belong in `shared/ui/`, and single-use page sections stay in the page. ## Where should layouts be placed? Layout components often need to compose data handling, state management, access control and user actions that are shared across multiple routes. In React Router nested child routes may share a common URL path such as `/users`, `/users/:id` and `/users/:id/settings`. Instead of repeating the same handling in each page you can use the router's nesting capabilities to apply a common layout and route-level logic in one place. The location of a layout should be determined based on its **scope and responsibility** rather than its structural complexity. - Layouts responsible for the entire application or routing structure should be placed in `app`. - Layouts specific to a particular page or route group should be placed in `pages`. - Layout UI that is reusable without business context can be placed in `shared/ui`. - Layouts centered around a specific user action or user flow and reused across multiple pages can be implemented in the corresponding `features` slice. A layout in `shared` that directly imports from `features`, `entities` or `pages` violates the layer import rule. Modules in `app` and `pages` can import modules from lower layers to compose a screen. > A module can only import modules from layers below the layer it belongs to. Before extracting a layout into a separate module consider the following: - Is this layout actually reused across multiple routes? - Is it specific to a particular page or route structure? - Is the layout itself the reusable unit or is it only the user action used within the layout? A layout used by only a small number of pages and tied to a particular screen structure may be simpler to define directly in the corresponding `page` or route configuration. 1. **Configure a route layout in the App layer** You can group multiple routes with a common URL path using the router's nesting capabilities and assign a single layout in `app`. A layout located in `app` can compose modules from `pages`, `features`, `entities` and `shared` without violating the layer import rule. 2. **Pass feature UI through render props or slots** In React you can use the render props pattern. In Vue you can use slots. In this approach the layout in `shared` provides only the common UI structure while the required feature UI is passed from `app` or `pages`. This allows the layout to compose the required screen without directly depending on a specific feature. 3. **Define it directly in a page** A layout used only by a specific page can be defined directly in the corresponding `page` without introducing a separate abstraction. When there is little duplicated code and the layout is unlikely to change frequently there is no need to extract it into a shared module. ## Features layer Independent, reusable user interactions. Create a feature when an interaction is already reused across consumers, has a focused responsibility, and needs one shared implementation. A second consumer on its own does not require one (the extraction rule in `SKILL.md`). ```text features/ auth/ ← Signing in ui/ LoginForm.tsx model/ auth.ts ← Session state + logic api/ login.ts index.ts register/ ← Signing up, a separate use case ui/ RegisterForm.tsx model/ register.ts api/ register.ts index.ts add-to-cart/ ui/ AddToCartButton.tsx model/ cart.ts index.ts like-post/ ui/ LikeButton.tsx model/ like.ts api/ toggle-like.ts index.ts ``` **Feature composition**: features consume entities and are composed in higher layers: ```typescript // pages/feed/ui/PostCard.tsx (composition lives in the page that uses it) import { UserAvatar } from '@/entities/user'; import { LikeButton } from '@/features/like-post'; import { CommentButton } from '@/features/comment-create'; export const PostCard = ({ post }) => ( <article> <UserAvatar userId={post.authorId} /> <h2>{post.title}</h2> <p>{post.content}</p> <div> <LikeButton postId={post.id} /> <CommentButton postId={post.id} /> </div> </article> ); ``` ## Entities layer Reusable business domain models. Create an entity when domain logic or state is already reused across consumers, has a focused responsibility, and needs one authoritative home (the extraction rule in `SKILL.md`). **Starting without this layer is completely valid.** ```text // Minimal entity: model only (most common form) entities/user/ model/ user.ts ← Types + domain logic index.ts // Entity with UI (use with caution) // Caution: adding UI to entities increases cross-import risk. // Other entities may want to import this UI, leading to @x dependencies. // Entity UI should only be imported from higher layers (features, pages, // app), never from other entities. entities/product/ model/ product.ts ui/ ProductCard.tsx index.ts ``` ## Shared layer structure Infrastructure with no business logic. Organized by segments only (no slices). Segments may import from each other. ```text shared/ ui/ ← UI kit: Button, Input, Modal, Card lib/ ← Utilities: formatDate, debounce, classnames api/ ← API client, route constants, CRUD helpers, base types auth/ ← Auth tokens, login utilities, session management config/ ← Environment variables, app settings ``` There is no `assets/` segment here. Assets live with the code that uses them, and a shared presentation asset goes to `shared/ui/` with the component that owns it (`references/asset-handling.md`). The official API requests guide groups request functions under `shared/api/endpoints/` and re-exports them from `shared/api/index.ts`. The examples in this skill also show flat domain-named files (`client.ts`, `product.ts`) and per-controller folders (`example/get-example.ts`); treat those as variations of the same idea, not competing standards. Whatever the internal shape, consumers import from the segment index, or from a component folder's own index where Rule 4-2 allows one. ```typescript // shared/ui/Button/Button.tsx export const Button = ({ children, onClick, variant = 'primary' }) => ( <button className={`btn btn-${variant}`} onClick={onClick}> {children} </button> ); // shared/ui/Button/index.ts export { Button } from './Button'; export type { ButtonProps } from './Button'; ``` Shared **may** contain application-aware code: route constants, API endpoints, branding assets, and transport types such as `ProductDTO`. It must **never** hold the business rules an entity or feature owns, nor import from those layers. For asset placement specifically (images, icons, fonts, PDFs), see `references/asset-handling.md`. ## Segments A segment groups related code within a slice (or within App/Shared). The standard segments cover the most common technical purposes: - **`ui`**: UI display (components, date formatters, styles). - **`api`**: backend interactions (request functions, data types, mappers). - **`model`**: data model (schemas, interfaces, stores, business logic). - **`lib`**: library code that other modules in this slice need. - **`config`**: configuration files and feature flags. Custom segments are allowed when needed (for example, `routes` and `i18n` in the Shared layer, or `auth` for token storage when split out from `shared/api`). ### Group by what it is *for*, not by what it *is* Segment names describe **purpose**, not the kind of code they hold. This is the desegmentation principle: ```text // BAD: grouping by technical kind (what the code is) shared/ components/ ← What kind of components? hooks/ ← Which feature do they serve? types/ ← Which domain do they describe? utils/ ← Utility for what? helpers/ ← Same problem actions/ ← Redux actions for what? // GOOD: grouping by purpose (what the code is for) shared/ ui/ ← For displaying UI api/ ← For talking to the backend lib/ ← For library code that supports the slice config/ ← For configuration ``` A segment named `types/` cannot answer "types for what?" without inspecting the contents. A segment named `model/` says: this is the data model. Inside `model/`, files are named by domain (`user.ts`, `order.ts`), not by technical role. This rule applies everywhere: in `shared/`, in slices, and when designing new custom segments. ## Naming conventions ### Domain-based file naming Within a segment, name files after what they are for, the concern or domain they serve, not after their technical mechanism: ```text // BAD: technical-role naming mixes domains model/types.ts ← Which types? User? Order? model/utils.ts api/endpoints.ts ← Every domain's requests in one file model/selectors.ts // GOOD: domain-based naming, each file owns one domain model/user.ts ← User types + logic + store model/order.ts ← Order types + logic + store api/fetch-profile.ts ← Clear what this API does model/todo.ts ← Redux slice + selectors + thunks ``` The fault in `api/endpoints.ts` is the single file, not the word. An `endpoints/` directory holding one file per domain is the shape the official API requests guide uses. ### Single-concern segments If a segment contains only one domain concern, the filename may match the slice name: ```text features/auth/ model/ auth.ts ← Single concern, matches slice name ``` ### Index files as public API Every slice must have an `index.ts` that re-exports its public interface: ```typescript // entities/user/index.ts export { UserAvatar } from "./ui/UserAvatar"; export { useUser, type User } from "./model/user"; ``` ## Slice groups A **slice group** is a folder that contains related slices on the same layer, used purely to make the structure easier to navigate as the number of slices grows. A slice group is **not** a slice itself: it has no segments (`model/`, `ui/`, `api/`), no public API (`index.ts`), and no shared code. Slice isolation rules apply unchanged inside a group: sibling slices in the same group cannot import from each other. Slice groups are optional. Use them only when the layer has grown large enough that a flat structure becomes hard to scan and there is an obvious grouping criterion. ### When to use - Several slices share the same business context and are scattered across the layer. - The slice names clearly suggest they belong to the same topic. - The layer has grown to the point where it is hard to scan at a glance. ### When NOT to use - Names alone are enough for quick navigation. - There is no natural grouping criterion. - The group would hold too few slices to make the layer easier to scan. ### Example: grouping payment-related entities ```text entities/ payment/ ← Slice group (no public API) invoice/ ← Slice model/ ui/ index.ts receipt/ ← Slice (model/, ui/, index.ts) transaction/ ← Slice (model/, ui/, index.ts) user/ ← Slice (not in any group) product/ ← Slice ``` Imports go through the full path: ```typescript import { Invoice } from "@/entities/payment/invoice"; import { Receipt } from "@/entities/payment/receipt"; ``` The same pattern applies to the Pages layer. For example, grouping `pages/order/{list,detail,create}` when there are multiple pages on the same topic such as list, detail, create, and edit. This is one possible example and does not represent the default structure for the Pages layer. ### Features: use with caution Slice groups can be applied to Features, but features often span multiple entities and lack a natural grouping criterion. A group like `features/cart/` tends to attract everything cart-related (DTOs, mappers, helpers) until it stops being a navigation aid and starts acting as the home for the entire cart domain, which weakens the principle that features are split by use case. Before grouping features, check that the group contains only feature slices and that the grouping earns its keep in navigation. ### Anti-patterns - **Do not put `index.ts` on the group folder.** That promotes the group to a slice and breaks the layer's contract. - **Do not put shared `utils.ts`, `constants.ts`, or `types.ts` files inside the group.** A slice group has no shared code. Move reusable infrastructure to `shared/`. If the layer is `entities` and the shared logic is genuinely domain logic, consider whether the boundaries are too granular and the slices should be merged into one isolated entity (see `references/excessive-entities.md`). The `@x` notation does not apply to slice groups. It is a cross-import surface between entity slices, not a sharing mechanism for siblings within a group. - **Do not relax slice isolation inside the group.** Grouping is navigation; it creates no sharing boundary. Siblings that need to depend on each other are resolved the same way as ungrouped slices, through `references/cross-import-patterns.md`, not with a `_common/` file. ## Path aliases Configure path aliases so imports follow the `@/layer/slice` pattern. Add an entry only for a layer the project actually has; an alias for a layer that does not exist invites someone to fill it. ```json // tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@/app/*": ["src/app/*"], "@/pages/*": ["src/pages/*"], "@/shared/*": ["src/shared/*"] } } } ``` That is the three-layer project from Section 5-3. Add `@/features/*`, `@/entities/*`, or `@/widgets/*` when those layers appear, not before. For framework-specific alias configuration (Vite, Next.js, Nuxt, Astro), see `references/framework-integration.md`. -
migration-guide.md 13.3 KB
# Migration Guide How to migrate to FSD v2.1 from either FSD v2.0 or a custom (non-FSD) architecture. This guide reflects the official `from-custom` step order: **pages first**, then everything else. The steps reveal where code already belongs; they are not a folder rename script. Where a step names a destination, the placement rules in `SKILL.md` still decide whether the code goes there. ## Part 1: FSD v2.0 → v2.1 (non-breaking) The v2.1 update emphasizes **"pages first"**: most logic stays in pages, reusable foundation in Shared. When a stable responsibility is genuinely shared by several consumers and needs one authoritative home, move that responsibility to the layer below. The migration is non-breaking and simplifies the codebase by relocating single-use code back to where it is consumed. Another addition in v2.1 is the standardization of cross-imports between entities with the `@x` notation. See `references/cross-import-patterns.md`. ### Step 1. Audit existing slices Use Steiger to detect slices that are used in only one place: ```bash npm install -D @feature-sliced/steiger npx steiger src ``` Look for these rules: - **`insignificant-slice`**: a slice with no references, or with one, which it suggests merging into the layer above. Pages may have a single reference without being flagged, and so may slices used only from `app`. - **`excessive-slicing`**: too many slices in a single layer. For each flagged slice, decide: - Several consumers, a stable and focused responsibility, and a reason to change of its own → leave it where it is. - One consumer → mark it to move back into that consumer. - Several consumers, but the copies would rather drift apart → local copies may still be the better answer (`references/growth-walkthrough.md`, Snapshot 1). ### Step 2. Move single-use code back to its consumer Move a single-reference feature or entity back into its sole consumer when the boundary no longer earns its own slice. That consumer is usually a page; it can be another feature, or an existing widget in a project that keeps its widgets layer: ```text // Before (v2.0): feature used by only one page features/user-profile-form/ ui/ProfileForm.tsx model/profile-form.ts api/update-profile.ts index.ts pages/profile/ ui/ProfilePage.tsx ← Thin wrapper, just composes // After (v2.1): code lives in the page that owns it pages/profile/ ui/{ProfilePage,ProfileForm}.tsx model/profile.ts ← Merged form logic api/update-profile.ts index.ts ``` For each moved slice: 1. Copy all files into the consuming page. 2. Update the page's `index.ts` to export what is needed externally. 3. Update all imports across the codebase to point to the new location. 4. Delete the now-empty feature/entity directory. 5. Run tests. ### Step 3. Keep genuinely reused code in place A slice that passed the check above stays in features/entities. Do not move it. The point of v2.1 is reducing premature extraction, not removing reuse. ### Step 4. Deprecate the processes layer The `processes` layer is deprecated. Migrate its code: - **Multi-page workflows** (checkout, onboarding wizard): move orchestration logic to the page that initiates the workflow. If the workflow is a stable user-action boundary that several pages reuse, it may become a feature. - **Background processes** (polling, sync): move to `app/` if global, or to the relevant page/feature if scoped. ```text // Before processes/ checkout/model/checkout-flow.ts sync/model/background-sync.ts // After features/checkout/model/checkout-flow.ts ← Stable flow, reused app/sync/background-sync.ts ← Global concern ``` ### Post-migration verification 1. Run `npx steiger src` and work through what it reports. For each single-reference slice, decide whether the boundary is still intentional or whether it survived only because it predates the migration. 2. Verify import directions. No upward or same-layer cross-imports. 3. Check that no empty layer directories remain. 4. Update documentation to reflect the new structure. ## Phasing out a small widgets layer (optional) The widgets layer is discouraged, but this migration is optional. A project with an established widgets layer that works well can keep it as is (see `references/layer-structure.md`). This section is for projects with only a few widgets that want to align with the recommended structure. Route each widget individually by its actual responsibility: 1. **Used by one page only** → inline it into that page's slice. This is the most common case: the block was extracted prematurely and never reused. 2. **Contains a user action reused across pages** (a form, a dialog, a toolbar with behavior) → move both the action and its UI to `features/`. 3. **Pure presentational UI with no business context** → move to `shared/ui/`. 4. **App-wide shell or layout** (header, footer, navigation frame) → move to `app/`, and compose it in the route configuration. 5. **Composes multiple features** → move the composition up into the page or the route layout in `app` instead of keeping a middle layer. Migrate one widget per commit, updating its imports as you go. Delete the `widgets/` directory (and its path alias) only when the last widget is gone. A half-empty widgets layer is fine in the meantime; import rules keep working throughout. ## Part 2: custom architecture → FSD This part follows the official `from-custom` migration order. The core philosophy is **pages first**: start by dividing the code by pages, then work outward. ### Before you start The most important question to ask the team is: *do you really need it?* Some projects are perfectly fine without FSD. Reasons to consider the switch: 1. New team members struggle to reach a productive level. 2. Modifications to one part of the code **often** break unrelated parts. 3. Adding new functionality is difficult due to the volume of context to hold in mind. **Avoid switching to FSD against the will of teammates**, even as a lead. Convince the team that the benefits outweigh migration and learning costs. Explain the migration plan to management; architectural changes are not immediately observable to them. If the decision is made, set up a path alias for `src/` first. This guide uses `@` as an alias for `./src`. ### Step 1. Divide the code by pages If `pages/` already exists, skip this step. Otherwise, create `pages/` and move as much component code as possible from `routes/` (or equivalent) into it. Aim for tiny route files that just re-export from page slices. ```text // Route file (thin) src/routes/products.[id].js export { ProductPage as default } from "@/pages/product" // Page slice src/pages/product/ ui/ProductPage.jsx index.js ← export { ProductPage } from "./ProductPage.jsx" ``` Pages may reference each other for now. Tackle that later. Focus on establishing a prominent division by pages. ### Step 2. Separate everything else from pages This step is a staging move, not the final ownership rule. Business code will land in `shared/` here, and Step 4 pulls it back out. Do not read "does not import pages" as a reason for code to live in Shared. Create `src/shared/` and move everything that does **not** import from `pages/` or `routes/` there. Create `src/app/` and move everything that **does** import the pages or routes there, including the routes themselves. The Shared layer has no slices, so segments may import from each other. ```text src/ app/ routes/ products.jsx products.[id].jsx App.jsx index.js pages/ product/ ui/ProductPage.jsx index.js catalog/ shared/ actions/, api/, components/, containers/, constants/, i18n/, modules/, helpers/, utils/, reducers/, selectors/, styles/ ``` ### Step 3. Tackle cross-imports between pages Find all cases where one page imports from another. Resolve each in one of two ways: 1. **Copy-paste** the imported code into the depending page to remove the dependency. 2. **Move to a Shared segment**: - UI kit code → `shared/ui/` - configuration constants → `shared/config/` - backend interaction → `shared/api/` Copy-pasting is **not architecturally wrong**. Sometimes it is more correct to duplicate than to abstract into a new reusable module, because the shared parts of pages can drift apart over time. Still, the DRY principle holds for business logic: avoid copy-pasting code that must stay in sync across multiple places. ### Step 4. Unpack the Shared layer The Shared layer can become bloated after Step 2. Find every object used in only one page and move it to that page's slice. **This applies to actions, reducers, and selectors too.** There is no benefit in grouping all actions together, but there is benefit in colocating relevant actions close to their usage. ```text src/ pages/ product/ actions/, reducers/, selectors/, ui/ ← moved from shared index.js catalog/ shared/ ← shared infrastructure only actions/, api/, components/, ... ``` ### Step 5. Organize code by technical purpose (segments) In FSD, division by technical purpose is done with **segments**. The common ones are: - **`ui`**: everything related to UI display (components, date formatters, styles). - **`api`**: backend interactions (request functions, data types, mappers). - **`model`**: the data model (schemas, interfaces, stores, business logic). - **`lib`**: library code that other modules in the slice need. - **`config`**: configuration files and feature flags. Custom segments are allowed when needed. **Do not create segments that group code by what it is**, like `components`, `actions`, `types`, or `utils`. Group code by what it is **for**, not by what it is. This is the desegmentation principle. Reorganize each page to separate code by segments: - The existing page UI files become the `ui` segment. - Actions, reducers, selectors, and the rest of the state wiring become the `model` segment. - Request functions become the `api` segment, and so does a thunk that carries its own request. Once the request is separated out, the Redux wiring left behind belongs with the reducer in `model` (`references/state-management.md`). Reorganize the Shared layer too: - `components/`, `containers/` → most of it becomes `shared/ui/`. - `helpers/`, `utils/` → group by function (dates, type conversions, etc.) and move groups to `shared/lib/`. - `constants/` → group by function and move to `shared/config/`. ## Optional steps ### Step 6. Form entities/features from Redux slices used on several pages A slice's subject does not settle its layer. Reused Redux slices typically describe business concepts (products, users) or user actions (comments, likes), and the official step says such a slice **can** move: - A stable, reusable business-domain responsibility may become an entity, one entity per folder. - A complete, reusable user interaction with a stable boundary may become a feature. - Anything single-use or still unclear stays with the page that uses it. Entities and features are meant to be independent. If your business domain contains inherent connections between entities (a song belongs to an artist), see the [business entities cross-references guide](https://fsd.how/docs/guides/examples/types#business-entities-and-their-cross-references). API functions related to these slices can stay in `shared/api`. ### Step 7. Refactor your modules Do not map `modules/` onto `features/`. A legacy `modules/` folder usually holds several kinds of code at once, so read each module by its responsibility and its consumers: a screen-specific block stays in `pages/`, a reusable action plus its UI goes to `features/`, a stable reused domain rule goes to `entities/`, infrastructure goes to `shared/`, and an app-wide shell (header, footer) goes to `app/`. Some modules describe large UI chunks; a project that keeps its widgets layer can migrate those there. ### Step 8. Form a clean UI foundation in `shared/ui` `shared/ui` should contain UI elements with no encoded business logic. Refactor components from `components/` and `containers/` to extract their business logic to higher layers. Where no stable shared boundary has appeared and the copies can evolve apart, keeping the behavior local to each consumer is an acceptable choice. ## Common pitfalls during migration 1. **Extracting too early.** Wait for real reuse, not anticipated reuse. The v2.1 philosophy is "pages first, extract later". 2. **Creating empty layers.** Do not create `features/` or `entities/` directories until there is content for them, and do not actively adopt the discouraged `widgets/` layer. 3. **Refactoring while migrating.** Separate relocation from refactoring. Move files first, improve them in separate commits. 4. **Ignoring import direction.** Enforce import rules from day one with ESLint or Steiger. 5. **Big-bang migration.** Migrate page by page, verifying each step. A hybrid structure (partly FSD, partly legacy) is acceptable during transition. 6. **Grouping by technical role.** `components/`, `actions/`, `utils/` as segment names defeat the purpose of FSD. Group by what code is for. ## Migrating from FSD v1 to v2 This guide does not cover v1 → v2. See the official [v1 to v2 migration guide](https://fsd.how/docs/guides/migration/from-v1). The v1 → v2 transition introduced the entities and processes layers (processes was later deprecated in v2.1). -
state-management.md 13.7 KB
# State Management Concrete code patterns for Redux and TanStack Query (React Query) within FSD structure. Authentication, type, and API request patterns are in `references/auth-and-api.md`. Code samples are React; the placement rules are framework-agnostic. ## State management: Redux FSD has no Redux guide of its own. The placement rule below is Step 6 of the official `from-custom` migration guide, and the only official Redux code is in the business entities guide. The rest of this section is Rules 4-1, 4-2, and 4-4 applied to Redux Toolkit. ### Where a Redux slice belongs **Redux does not decide the layer; ownership does.** Work out which slice owns the state with Section 2 of `SKILL.md`, then put the Redux code in that slice's `model/` segment. A `todo` noun does not make an entity, and a `toggle-todo` verb does not make a feature. Once a boundary has been earned, Step 6 gives two destinations: a stable reusable business-domain responsibility may move to Entities, a stable reusable user-interaction boundary may move to Features. Both assume the slice is already reused across pages. A slice used by a single page stays in that page's `model/` segment. ### Business-entity slice in entities The request is plain resource access, so it lives in `shared/api` with its DTO (Request placement rule in `references/auth-and-api.md`). The entity imports it; `model/` holds only the Redux wiring. The entity uses the transport type as it is here; convert to a separate domain type only when a business rule needs a shape the backend does not send. ```typescript // shared/api/todo.ts import { apiClient } from "./client"; export interface TodoDto { id: string; title: string; completed: boolean } export const getTodos = (): Promise<TodoDto[]> => apiClient.get("/todos").then((r) => r.data); ``` ```typescript // entities/todo/model/todo.ts import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { getTodos, type TodoDto } from "@/shared/api"; interface TodoState { items: TodoDto[]; loading: boolean } export const fetchTodos = createAsyncThunk("todos/fetch", getTodos); const todoSlice = createSlice({ name: "todos", initialState: { items: [], loading: false } as TodoState, reducers: { setCompleted: (state, { payload }: { payload: { id: string; completed: boolean } }) => { const todo = state.items.find((t) => t.id === payload.id); if (todo) todo.completed = payload.completed; }, }, extraReducers: (builder) => { builder .addCase(fetchTodos.pending, (state) => { state.loading = true; }) .addCase(fetchTodos.fulfilled, (state, action) => { state.items = action.payload; state.loading = false; }); }, }); export const { setCompleted } = todoSlice.actions; export const selectTodos = (state: { todos: TodoState }) => state.todos.items; export const todoReducer = todoSlice.reducer; ``` A thunk that still carries its own request belongs in the `api` segment (`references/migration-guide.md`, Part 2 Step 5). Once the request lives in `shared/api`, the thunk is only Redux wiring and stays in `model/` next to the reducer that handles it, which is the shape of the official business entities guide. The selector takes only the state it reads, not `RootState`. `RootState` is declared in `app/`, so an entity importing it would depend on a higher layer (Rule 4-1). The trade-off is that the selector no longer type-checks against the whole store. Type it at the `app/` layer when you need that guarantee. The slice's public API re-exports what consumers need: ```typescript // entities/todo/index.ts export { todoReducer, selectTodos, setCompleted, fetchTodos } from "./model/todo"; ``` **Key:** Do not split Redux code by Redux mechanism into `reducers.ts`, `selectors.ts`, and `thunks.ts`. That is the technical-role naming Rule 4-4 rules out. Keep a reducer, its selectors, and its thunks together in one domain-named file, and when the model outgrows it, split by domain concern (`todo.ts`, `todo-filter.ts`) rather than by mechanism. ### User-action slice in features Assuming the action is already reused across pages and has earned a feature boundary, it consumes the entity through the entity's public API and exposes its own hook through the feature's: ```typescript // features/toggle-todo/model/use-toggle-todo.ts import { useDispatch } from "react-redux"; import { setCompleted } from "@/entities/todo"; export const useToggleTodo = () => { const dispatch = useDispatch(); return (id: string, current: boolean) => dispatch(setCompleted({ id, completed: !current })); }; ``` ### Registering slices in app ```typescript // app/providers/store.ts import { configureStore } from "@reduxjs/toolkit"; import { todoReducer } from "@/entities/todo"; import { userReducer } from "@/entities/user"; export const store = configureStore({ reducer: { todos: todoReducer, user: userReducer, }, }); export type RootState = ReturnType<typeof store.getState>; ``` The store imports each slice's reducer through its public API (`index.ts`), never reaching into `model/` directly (Rule 4-2). Do not let individual slices create their own stores. ## State management: TanStack Query (React Query) This section follows the official React Query guide. Guidance applies to `@tanstack/react-query` v5 (formerly React Query). The package name is `@tanstack/react-query`. ### Where to store query keys Three placements are valid. Choose by project size and by which slice owns the request, not by which folders happen to exist. **Option 1: Flat in `shared/api/queries/`** (small projects, few endpoints): ```text shared/api/ queries/ example.ts another-example.ts index.ts ← export { exampleQueries } from './queries/example'; ``` **Option 2: Per controller in `shared/api/<controller>/`** (many endpoints): ```text shared/api/example/ index.ts ← export { exampleQueries } from './example.query'; example.query.ts ← Query factory: keys + functions get-example.ts create-example.ts update-example.ts delete-example.ts ``` **Option 3: Per entity in `entities/<entity>/api/`** when the request carries domain rules, the entity boundary already exists, and each request corresponds to a single entity. Generic CRUD and plain resource access stay in `shared/api` however many slices call them (`auth-and-api.md`, request placement rule, Question 2). When entities reference each other, see `references/cross-import-patterns.md` for `@x` as a last resort. > **Where this comes from.** Two official guides differ here. The React > Query guide calls the per-entity split the cleanest option once a > project has entities, and shows CRUD files inside `entities/*/api/`. > The excessive-entities guide excludes CRUD from entities and keeps it > in `shared/api/endpoints/`. This skill follows the second, and > Question 2 is how it decides: an existing entities folder is not by > itself a reason to move a request into it. ### Where to store mutations Mixing mutations with queries is not recommended. Two patterns are accepted: 1. **A mutation hook in the `api/` segment near the place of use.** Use `setQueryData` for cache updates: ```typescript // src/pages/example/api/use-update-example.ts export const useUpdateExample = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, newTitle }) => apiClient.patch(`/posts/${id}`, { title: newTitle }).then((r) => r.data), onSuccess: (newPost, { id }) => queryClient.setQueryData(POST_QUERIES.detail({ id }).queryKey, newPost), }); }; ``` 2. **A `mutationFn` defined in `shared/` or `entities/`** and called from `useMutation` in the component. ### Query factory pattern A query factory is an object whose values return query keys. Each key is wrapped in `queryOptions`, a built-in helper from `@tanstack/react-query` v5 that lets you share `queryKey` and `queryFn` between `useQuery`, `useSuspenseQuery`, `prefetchQuery`, `setQueryData`, and similar APIs without rewriting them: ```typescript // src/shared/api/post/post.queries.ts import { queryOptions } from "@tanstack/react-query"; import { getPosts, getDetailPost, type DetailPostQuery } from "./get-posts"; export const POST_QUERIES = { all: () => ["posts"], lists: () => [...POST_QUERIES.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...POST_QUERIES.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: (prev) => prev, }), detail: (query?: DetailPostQuery) => queryOptions({ queryKey: [...POST_QUERIES.all(), "detail", query?.id], queryFn: () => getDetailPost({ id: query?.id }), }), }; ``` Consume with `useQuery(POST_QUERIES.detail({ id }))`. For pagination, `placeholderData: prev => prev` prevents UI flicker when navigating pages. **Benefits of a query factory:** the keys and query definitions for a domain are reachable through one object, so consumers share them instead of rebuilding keys. Refetching and cache updates become a one-line call (`queryClient.invalidateQueries({ queryKey: POST_QUERIES.all() })`). The request functions themselves stay in their own files; the factory wires them to keys. ### Infinite scroll Use `infiniteQueryOptions` with `initialPageParam` and `getNextPageParam`. Add the infinite key to the same factory shown above: ```typescript import { infiniteQueryOptions } from "@tanstack/react-query"; // Inside POST_QUERIES: infinite: (limit: number) => infiniteQueryOptions({ queryKey: [...POST_QUERIES.lists(), "infinite", limit], queryFn: ({ pageParam }) => getPosts(pageParam, limit), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.skip + lastPage.limit < lastPage.total ? lastPage.skip / lastPage.limit + 1 : undefined, }), ``` Consume with `useInfiniteQuery` and flatten via `data?.pages.flatMap(...)`. ### Suspense mode `queryOptions` and `useSuspenseQuery` are compatible, and the factory does not change. Components use `useSuspenseQuery` instead of `useQuery` and skip `isLoading` entirely. Wrap interested subtrees with an `ErrorBoundary` + `Suspense` provider in the App layer: ```tsx // src/app/providers/suspense-provider.tsx import { Suspense } from "react"; import { ErrorBoundary } from "react-error-boundary"; export const SuspenseProvider = ({ children }) => ( <ErrorBoundary fallback={<div>Something went wrong</div>}> <Suspense fallback={<div>Loading...</div>}>{children}</Suspense> </ErrorBoundary> ); ``` ### Reading mutation state with useMutationState `useMutationState` lets any component read the state of a mutation without passing props, useful for global save indicators. Store mutation keys next to the query factory: ```typescript // src/shared/api/post/post.queries.ts export const POST_MUTATIONS = { updateTitle: () => ["post", "update-title"], create: () => ["post", "create"], }; ``` Tag the mutation with `mutationKey`, then read its state from any component: ```tsx // src/features/update-post/api/use-update-post-title.ts export const useUpdatePostTitle = () => useMutation({ mutationKey: POST_MUTATIONS.updateTitle(), mutationFn: ({ id, newTitle }) => apiClient.patch(`/posts/${id}`, { title: newTitle }), }); // src/app/ui/save-indicator.tsx (app-wide; page-local if one page) import { useMutationState } from "@tanstack/react-query"; import { POST_MUTATIONS } from "@/shared/api/post"; export const SaveIndicator = () => { const isPending = useMutationState({ filters: { mutationKey: POST_MUTATIONS.updateTitle(), status: "pending" }, select: (m) => m.state.status, }).length > 0; return isPending && <span>Saving...</span>; }; ``` ### QueryProvider in the app layer ```tsx // src/app/providers/query-provider.tsx import { QueryClient, QueryClientProvider, MutationCache, QueryCache } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { toast } from "sonner"; const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (e) => toast.error(e.message) }), mutationCache: new MutationCache({ onError: (e) => toast.error(e.message) }), defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000 } }, }); export const QueryProvider = ({ children }) => ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools /> </QueryClientProvider> ); ``` `QueryCache.onError` and `MutationCache.onError` give one place to wire up global toast notifications instead of repeating error handling on every hook. ### Code generation The official guide notes that OpenAPI/Swagger generators are less flexible than the hand-written factory above. Whichever you pick, generated clients are transport code: keep them in `@/shared/api/`, or in a separate generated package. Do not move generated endpoints into entities because they happen to name business resources. ### Custom API client Standardize base URL, headers, and JSON handling in a single class in `shared/api/`: ```typescript // src/shared/api/api-client.ts export class ApiClient { #baseUrl: string; constructor(url: string) { this.#baseUrl = url; } async #handle<T>(response: Response): Promise<T> { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } get = <T>(path: string) => fetch(`${this.#baseUrl}${path}`).then((r) => this.#handle<T>(r)); // post, put, delete follow the same pattern with method/headers/body. } export const apiClient = new ApiClient(API_URL); ``` **Key principle:** Place query and mutation hooks with the slice that owns the responsibility. Page-specific queries stay in the page. Plain resource access and generic CRUD stay in `shared/api/`. Reach for `entities/<name>/api/` only when an established entity boundary owns that request; an Entities layer existing is not a placement rule.
-
-
SKILL.md 21.8 KB
--- name: feature-sliced-design description: > Official Feature-Sliced Design (FSD) v2.1 skill for applying the methodology to frontend projects. Use when the task involves organizing project structure with FSD layers, deciding where code belongs, placing static assets (images, icons, fonts, PDFs), grouping closely related slices, defining public APIs and import boundaries, resolving cross-imports or evaluating the @x pattern, deciding whether to create or remove an entity, evaluating whether the entities layer is needed at all, deciding where page layouts belong or whether to use the widgets layer (discouraged), deciding whether logic should remain local or be extracted, migrating from FSD v2.0 or a non-FSD codebase, integrating FSD with frameworks (Next.js App Router and Pages Router, React Router, Nuxt, Vite, Astro), or implementing common patterns such as authentication, API handling, Redux, and TanStack Query (React Query) within FSD. --- # Feature-Sliced Design (FSD) v2.1 > **Source**: [fsd.how](https://fsd.how) | Strictness can be adjusted based on > project scale and team context. **How to use this skill.** For placement decisions, start with the decision tree in Section 2 and use the placement table in Section 3 as a quick reference. To check a structure for violations, use the rules in Section 4. To resolve same-layer cross-imports, use Section 7. For task-specific guidance, load only the relevant reference files from Section 10; do not preload the rest. ## 1. Core philosophy & layer overview FSD v2.1 core principle: **"Start simple, extract when needed."** ### The extraction rule Place code in `pages/` first. Duplication across pages is acceptable and does not by itself require extraction to a lower layer. Extract only when all three conditions hold: 1. The same code is used in multiple places right now, not hypothetically. 2. It has a reason to change that is independent of any one consumer. 3. The boundary has a focused responsibility. ### The six layers **Not all layers are required.** Most projects can start with only `shared/`, `pages/`, and `app/`. Add `features/` and `entities/` only when they provide clear value. Do not create empty layer folders "just in case." The `widgets/` layer is **discouraged** (see the callout below). FSD uses 6 standardized layers, listed here from highest to lowest: ```text app/ → App initialization, providers, routing pages/ → Route-level composition, owns its own logic widgets/ → Reusable UI blocks (discouraged, see the callout below) features/ → Reusable user interactions (see the extraction rule above) entities/ → Reusable business domain models (see the extraction rule above) shared/ → Infrastructure with no business logic (UI kit, utils, API client) ``` **The official layer reference discourages using the Widgets layer**, and this skill follows it. Widgets may seem useful for representing independent UI blocks. However, in real frontend code, UI blocks often include logic required for user flows, such as data fetching, state management, and event handling. In this case, the responsibilities of Features, which handle user flows, and Widgets, which handle UI blocks, can overlap, making the boundary between the two layers unclear. Not creating a widget does not mean moving the block elsewhere untouched. A screen-specific composition stays in `pages`; a reused action and the UI to perform it go to `features`; context-free UI goes to `shared`; an app-wide layout goes to `app`. Discouraged is not deprecated: an existing widgets layer stays valid. See `references/layer-structure.md` for that case and for layout placement. ### The import rule A module may only import from layers strictly below it. Cross-imports between slices on the same layer are forbidden, with one narrow exception in Section 7. ```typescript // Allowed import { Button } from "@/shared/ui/Button"; // features → shared import { useUser } from "@/entities/user"; // pages → entities // Violation import { loginUser } from "@/features/auth"; // entities → features import { likePost } from "@/features/like-post"; // features → features ``` **Note**: The `processes/` layer is **deprecated** in v2.1. For migration details, read `references/migration-guide.md`. ## 2. Decision framework When writing new code, follow this tree: **Step 1: Where is this code used?** - Used in only one page → keep it in that `pages/` slice. - Used in 2+ pages but duplication is manageable → keeping separate copies in each page is also valid. - An entity or feature with a single consumer → keep it at the consumer (Steiger flags this as `insignificant-slice`). **Step 2: Is it reusable infrastructure with no business logic?** The official layer reference draws the line for Shared like this: no business logic, but business-themed is fine (a company logo, a page layout), and so is UI logic (autocomplete, a search bar). Exchanging data with the backend and CRUD boilerplate are not business logic either. Business logic is a rule the product enforces on its own data, such as applying a discount to an order. If the code fits none of the exclusions and still does not clearly enforce a product rule, the term does not decide; go back to Step 1 and place it by where it is used. - UI components → `shared/ui/` - Utility functions → `shared/lib/` - API client, route constants → `shared/api/` or `shared/config/` - Auth tokens, session management → `shared/auth/` - CRUD once several slices call it → `shared/api/` (a single caller keeps it, see Step 1) **Step 3: Is it a complete user action that several consumers share, with a focused responsibility and a reason to change of its own?** - Yes → `features/` - Uncertain, single use, or speculative reuse → keep in the page. **Step 4: Is it a business domain model that several consumers share, with a focused responsibility and a reason to change of its own?** - Yes → `entities/` - Uncertain, single use, or speculative reuse → keep in the page. **Step 5: Is it app-wide configuration?** - Global providers, router, theme → `app/` **Golden Rule: When in doubt, keep it in `pages/`. Extract only when the extraction rule holds.** ## 3. Quick placement table | Scenario | Single use | Confirmed multi-use | | -------------------------- | ------------------------------------------- | ------------------------------------- | | User profile form | `pages/profile/ui/ProfileForm.tsx` | `features/profile-form/` | | Product card | `pages/products/ui/ProductCard.tsx` | `entities/product/ui/` if the entity owns it | | API request (read or CRUD) | `pages/product-detail/api/fetch-product.ts` | `shared/api/` (no domain rules) | | Auth token/session | `shared/auth/` | `shared/auth/` | | Auth login form | `pages/login/ui/LoginForm.tsx` | `features/auth/` | | Generic Card layout | | `shared/ui/Card/` | | Modal manager | | `shared/ui/modal-manager/` | | Modal content | `pages/[page]/ui/SomeModal.tsx` | | | Date formatting util | | `shared/lib/format-date.ts` | "Confirmed multi-use" means the extraction rule holds, not that a second consumer appeared: two similar copies that keep drifting apart stay in their pages (`references/growth-walkthrough.md`, Snapshot 1). Entity UI carries the Section 6 caution even when the rule does hold. ## 4. Architectural rules (MUST) These rules are the foundation of FSD. Violations weaken the architecture. If you must break a rule, ensure it is an intentional design decision and document the reason in code (a comment or ADR). ### 4-1. Import only from lower layers `app → pages → widgets → features → entities → shared`. Upward imports are forbidden. So are cross-imports between slices on the same layer, except through the other slice's public API as a last resort (Section 7, Strategy D). ### 4-2. Public API: every slice exports through index.ts External consumers may only import from a slice's `index.ts`. Direct imports of internal files are forbidden. ```typescript // Correct import { LoginForm } from "@/features/auth"; // Violation: bypasses public API import { LoginForm } from "@/features/auth/ui/LoginForm"; ``` **Shared layer:** Shared has no slices. Define a separate public API per segment (`shared/ui/index.ts`, `shared/api/index.ts`, etc.) rather than one top-level `shared/index.ts`. This keeps imports from Shared organized by intent. Where one index over a segment's unrelated modules hurts bundling, give each component, library, or controller folder its own index instead (`shared/ui/Button/index.ts` as `@/shared/ui/Button`, `shared/api/post/` as `@/shared/api/post`). That folder is then the boundary; reaching past it (`@/shared/ui/Button/Button.tsx`) is still a violation. See `references/layer-structure.md` for the shape. **Environment-specific entry points:** a slice normally exposes one `index.ts`, and ad-hoc variations are not recommended. If a single index cannot preserve a runtime boundary, add an entry point such as `index.server.ts`. See `references/framework-integration.md`. ### 4-3. No cross-imports between slices on the same layer If two slices on the same layer need to share logic, follow the resolution order in Section 7. Never reach into another slice's internals. ### 4-4. Domain-based file naming (no desegmentation) Name files after what they are for, the domain or concern they serve, not after their technical role. Technical-role names like `types.ts`, `utils.ts`, `helpers.ts` mix unrelated concerns in a single file and reduce cohesion. ```text // BAD: technical-role naming model/types.ts ← Which types? User? Order? Mixed? model/utils.ts // GOOD: domain-based naming model/user.ts ← User types + related logic model/order.ts ← Order types + related logic api/fetch-profile.ts ← Clear purpose ``` ### 4-5. No business logic in shared/ Shared contains only infrastructure: UI kit, utilities, API client setup, route constants, assets. Business calculations, domain rules, and workflows belong in `entities/` or higher layers. Section 2, Step 2 says what counts. ```typescript // BAD: business logic in shared // shared/lib/userHelpers.ts export const calculateUserReputation = (user) => { ... }; // GOOD: move it to whoever owns the rule // pages/profile/model/reputation.ts ← while the profile page owns it // entities/user/model/reputation.ts ← once a user boundary is earned export const calculateUserReputation = (user) => { ... }; ``` ## 5. Recommendations (SHOULD) ### 5-1. Pages first: place code where it is used Place code in `pages/` first. Extract to lower layers only when truly needed. Extraction is a design decision that affects the whole project, so the threshold should be high. **What stays in pages:** - Large UI blocks used only in one page - Page-specific forms, validation, data fetching, state management - Page-specific business logic and API integrations - Code that looks reusable but is simpler to keep local **Evolution pattern:** Start with everything in `pages/profile/`. Extract the shared model to `entities/user/` when a second page consumes it *and* the extraction rule holds. A response type that several pages read is not one of those cases: it stays in `shared/api`. Keep page-specific API calls and UI in the page. ### 5-2. Be conservative with entities The entities layer is highly accessible (almost every other layer can import from it), so changes propagate widely. 1. **Start without entities.** `shared/` + `pages/` + `app/` is valid FSD. Thin-client apps rarely need entities. 2. **Do not split slices prematurely.** Keep code in pages. Extract to entities only when the extraction rule holds. 3. **Business logic does not automatically require an entity.** Keeping types in `shared/api` and logic in the current slice's `model/` segment may be sufficient. 4. **CRUD is infrastructure, not entities.** Place it by the request placement rule: with its consumer while there is one, in `shared/api/` once several slices call it. 5. **Place auth data in `shared/auth/` or `shared/api/`.** Tokens and login DTOs are auth-context-dependent and rarely reused outside authentication. For detailed guidance on keeping the entities layer clean (when to skip it entirely, how to isolate business contexts, why CRUD belongs in `shared/api`), see `references/excessive-entities.md`. ### 5-3. Start with minimal layers ```text // Valid minimal FSD project src/ app/ ← Providers, routing pages/ ← All page-level code shared/ ← UI kit, utils, API client // Add layers only when an actual use case requires them: // + features/ ← User-action boundaries that need one shared home // + entities/ ← Domain boundaries that need one shared home // (widgets/ is discouraged; see Section 1 for where that code goes instead) ``` ### 5-4. Validate with the Steiger linter [Steiger](https://github.com/feature-sliced/steiger) is the official FSD linter. Key rules: - **`insignificant-slice`**: Flags a slice with no references, or with one, and suggests merging it into the layer above. Pages may hold a single reference, and so may slices used only from `app/`. - **`excessive-slicing`**: Suggests merging or grouping when a layer has too many slices. ```bash npm install -D @feature-sliced/steiger npx steiger src ``` ## 6. Anti-patterns (AVOID) - **Do not create entities prematurely.** Data structures used in only one place belong in that place. - **Do not put CRUD in entities.** Plain CRUD is `shared/api/`. An operation that carries business rules is placed by who owns the rule, which may be an entity, a feature, or the page running the workflow. - **Do not create a `user` entity just for auth data.** Tokens and login DTOs belong in `shared/auth/` or `shared/api/`. - **Do not abuse `@x`.** It is a necessary compromise, not a recommended pattern. The notation is for the entities layer only, and only when boundary merge is genuinely impossible. Features and widgets handle cross-imports through strategies A through D (see Section 7). - **Do not extract single-use code.** A feature or entity used by only one page should stay in that page. - **Do not use technical-role file names.** Use domain-based names (see Rule 4-4). - **Be cautious adding UI to entities.** Entity UI tempts cross-imports from other entities. If you add UI segments to entities, only import them from higher layers (features, pages, app), never from other entities. - **Do not create god slices.** Slices with excessively broad responsibilities should be split into focused slices (e.g., split `user-management/` into `auth/`, `profile-edit/`, `password-reset/`). - **Do not create a top-level `assets/` segment.** Place static assets next to the code that uses them; global stylesheets and fonts go to `app/`. See `references/asset-handling.md`. ## 7. Cross-import resolution Cross-imports are a code smell, not an absolute prohibition. The right strategy depends on the layer and the situation. ### Entities layer: prefer boundary merge, @x is last resort Cross-imports in `entities` are usually caused by splitting entities too granularly. Before reaching for `@x`, consider whether the boundaries should be merged. `@x` is a **necessary compromise, not a recommended approach**. Use it only when boundaries genuinely cannot be merged, and document why. Overuse locks entity boundaries together and increases refactoring cost. ### Features and widgets: four strategies (A, B, C, D) In `features` and `widgets`, choose based on context: - **Strategy A: slice merge.** Two slices always change together → merge. - **Strategy B: push to entities.** A shared domain responsibility → move it to the entity that owns it, keep UI in the feature. - **Strategy C: compose from upper layer (IoC).** The parent (pages or app) imports both slices and connects them via render props, slots, or DI. - **Strategy D: Public API access.** When reuse is genuinely unavoidable, allow it only through the slice's `index.ts`. Never reach into `model/`, `store/`, or internal files. The `@x` notation is for the entities layer only. Features and widgets use strategies A through D above. ### Strictness depends on project context Cross-imports are dependencies that are generally best avoided, but sometimes used intentionally. Strictness varies by project context: - **Early-stage products** with heavy experimentation: allowing some cross-imports may be a pragmatic speed trade-off. - **Long-lived or regulated systems** (fintech, large-scale services): stricter boundaries pay off in maintainability and stability. If a cross-import is introduced, treat it as a deliberate choice and document the reasoning in code (a comment explaining why other strategies do not apply). For detailed code examples of each strategy, read `references/cross-import-patterns.md`. ## 8. Segments & structure rules ### Standard segments Segments group code within a slice by technical purpose: - **`ui/`**: UI components, styles, display-related code - **`model/`**: Data models, state stores, business logic, validation - **`api/`**: Backend integration, request functions, API-specific types - **`lib/`**: Internal utility functions for this slice - **`config/`**: Configuration, feature flags ### Layer structure rules - **App and Shared**: No slices, organized directly by segments. Segments within these layers may import from each other. - **Pages, Widgets, Features, Entities**: Slices first, then segments inside each slice. - **Slice groups (optional)**: A group folder may contain related slices on the same layer for navigation purposes only. The group has no segments and no public API. See `references/layer-structure.md` for details. ### File naming within segments Always use domain-based names that describe what the code is about: ```text model/user.ts ← User types + logic + store model/order.ts ← Order types + logic + store api/fetch-profile.ts ← Profile fetching api/update-settings.ts ← Settings update ``` If a segment has only one domain concern, the filename may match the slice name (e.g., `features/auth/model/auth.ts`). ## 9. Shared layer guide Shared contains infrastructure with **no business logic**. It is organized by segments only (no slices). Segments within shared may import from each other. **Allowed in shared:** - `ui/`: UI kit (Button, Input, Modal, Card) - `lib/`: Utilities (formatDate, debounce, classnames) - `api/`: API client, route constants, CRUD helpers, base types - `auth/`: Auth tokens, login utilities, session management - `config/`: Environment variables, app settings - Assets live with the code that uses them, not in an `assets/` segment. See `references/asset-handling.md`. Shared **may** contain application-aware code: route constants, API endpoints, branding assets, and transport types such as `ProductDTO`. It must **never** hold the business rules an entity or feature owns, nor import from those layers. ## 10. Conditional references Read the following reference files **only** when the specific situation applies. Do **not** preload all references. - **When reviewing or reorganizing folder and file structure** that already exists, deciding what goes inside a layer or slice, deciding where a page layout belongs, routing widget-like code to another layer, or grouping closely related slices into a parent folder for navigation (e.g., "where does this folder go", "how do I group these payment entities"): → Read `references/layer-structure.md` - **When setting up a new project from scratch** (e.g., "set up an FSD project", "start a new app with FSD"), or when asked whether to add entities or features yet, or to show how a structure earns each layer over time rather than its finished shape: → Read `references/growth-walkthrough.md` - **When resolving cross-import issues** between slices on the same layer, evaluating the `@x` pattern, choosing between Strategy A/B/C/D for features and widgets, or deciding whether boundaries should be merged: → Read `references/cross-import-patterns.md` - **When deciding whether to create or remove an entity**, dealing with too many entities, evaluating whether to skip the entities layer entirely, placing CRUD operations, or isolating business contexts to avoid `@x` chains: → Read `references/excessive-entities.md` - **When deciding where to place static assets** (images, icons, fonts, PDFs, stylesheets) for a single slice, for sharing across slices, or globally: → Read `references/asset-handling.md` - **When migrating** from FSD v2.0 to v2.1, converting a non-FSD codebase to FSD, phasing out an existing widgets layer, or deprecating the processes layer: → Read `references/migration-guide.md` - **When integrating FSD with a specific framework** (Next.js with App Router or Pages Router, React Router, Nuxt, Vite, Astro) for wiring routes to FSD pages, placing proxy/middleware and instrumentation files, structuring API route handlers, or configuring path aliases: → Read `references/framework-integration.md` - **When implementing authentication, type definitions, or API request handling** as concrete code within FSD structure (token storage, login flow, DTO placement, where a request function lives): → Read `references/auth-and-api.md` - **When wiring state management** (Redux slices, TanStack Query / React Query, including query factories, infinite scroll, Suspense mode, and `useMutationState`) into FSD structure: → Read `references/state-management.md`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.