chore: agentic upgrade
dev / deploy (push) Successful in 2m15s
compute / deploy (push) Has been cancelled

This commit is contained in:
Daniil
2026-05-17 02:11:33 +03:00
parent c16fcba693
commit 21e936a827
106 changed files with 12094 additions and 291 deletions
@@ -0,0 +1,183 @@
# Asset Handling
How to place static assets (images, icons, fonts, PDFs, stylesheets) inside an
FSD project. Assets follow the same placement rules as code: group by use
case, not by type, and keep them next to the code that uses 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. **Used by exactly one slice?** Keep the asset inside that slice, usually
in the `ui/` segment, or in `model/` if it is part of business logic.
2. **Reused across the app (icons, placeholder images)?** Move to
`shared/ui/`.
3. **Global stylesheet, font, or app-level resource?** Place in the `app/`
layer (`app/styles/`, `app/fonts/`).
4. **Served as-is by the bundler (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 the same asset appears across multiple slices, move it to `shared/ui/`.
Place reusable images in a topical subfolder, or place a single asset next to
the shared component that uses 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.
They bootstrap the application's visual layer the same way providers
bootstrap the runtime layer.
---
## Public Folder
Most bundlers expose a `public/` folder at the project root. Files here are
served as-is, without bundling or hashing.
- Vite, Next.js, Nuxt: `public/` at the project root.
- Astro: `public/` at the project root (path is fixed and cannot be changed).
`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.
```text
public/
favicon.ico
robots.txt
og-image.png
src/
app/
pages/
shared/
```
Some projects keep a project-local `app/public/` folder when the bundler
allows assets to live alongside the entrypoint. Both layouts are valid.
---
## Summary Table
| Asset | Location |
| -------------------------------------- | ----------------------------------------- |
| Image used by one page/widget/feature | Inside the slice's `ui/` segment |
| PDF or template tied to business logic | Inside the slice's `model/` segment |
| Icon reused across the app | `shared/ui/` (topical subfolder if many) |
| 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/fonts/`, `public/`, or `app/public/` |
| Favicon, robots.txt, sitemap | `public/` (or `app/public/`) |
---
## 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-local asset to `shared/` "in case" it gets
reused.** Move it only when actual reuse appears.
- **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 split assets and the components that use them.** A page that
ships a hero image should keep that image in the page so removing the page
removes the image.
---
## 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
@@ -0,0 +1,374 @@
# Cross-Import Resolution Patterns
How to resolve cross-imports between slices on the same layer. Cross-imports
are a code smell, not an absolute prohibition. The strategies below are
ordered, but the right choice depends on the project context.
## What is a cross-import?
A cross-import is an import between different slices within the same layer.
For example:
- importing `features/product` from `features/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.
Changes to `product`'s internal implementation can break `cart`
without warning. 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 each slice can be developed, tested, and deployed
independently. Cross-imports break this isolation. Testing `cart` now
requires setting up `product`, and changes in one slice can cause
unexpected test failures 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/profile/
features/profile-settings/
// After: one cohesive feature
features/profile/
ui/
Profile.tsx
ProfileSettings.tsx
model/
profile.ts
profile-settings.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: Push shared domain flows down into entities
If multiple features share a domain-level flow, move that flow into a domain
slice inside `entities`. Key principles:
- `entities` contains domain types and domain logic only.
- UI remains in `features` and `widgets`.
- Features import and use the domain logic from `entities`.
For example, if both `features/auth` and `features/profile` need session
validation, place session-related domain functions in `entities/session`
and reuse them from both features.
```text
entities/
session/
model/
validate-session.ts
session.ts
index.ts
features/
auth/
ui/LoginForm.tsx
model/login.ts ← imports validateSession from entities/session
index.ts
profile/
ui/ProfilePanel.tsx
model/profile.ts ← imports validateSession from entities/session
index.ts
```
### Strategy C: Compose from an upper layer (IoC)
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/user-profile/index.ts
export { UserProfilePanel } from "./ui/UserProfilePanel";
// features/activity-feed/index.ts
export { ActivityFeed } from "./ui/ActivityFeed";
// pages/UserDashboardPage.tsx
import { UserProfilePanel } from "@/features/user-profile";
import { ActivityFeed } from "@/features/activity-feed";
export const UserDashboardPage = () => (
<div>
<UserProfilePanel />
<ActivityFeed />
</div>
);
```
`features/user-profile` and `features/activity-feed` do not know about each
other. The page composes them.
#### Render props (React)
When one feature needs to render content from another, use render props to
invert the dependency:
```typescript
// features/comment-list/ui/CommentList.tsx
interface CommentListProps {
comments: Comment[];
renderUserAvatar?: (userId: string) => React.ReactNode;
}
export const CommentList = ({ comments, renderUserAvatar }: CommentListProps) => (
<ul>
{comments.map((comment) => (
<li key={comment.id}>
{renderUserAvatar?.(comment.userId)}
<span>{comment.text}</span>
</li>
))}
</ul>
);
// pages/PostPage.tsx
import { CommentList } from "@/features/comment-list";
import { UserAvatar } from "@/features/user-profile";
export const PostPage = () => (
<CommentList
comments={comments}
renderUserAvatar={(userId) => <UserAvatar userId={userId} />}
/>
);
```
`CommentList` does not import from `user-profile`. The page injects the
avatar component.
#### Slots (Vue)
Vue's slot system provides a natural way to compose features without
cross-imports:
```vue
<!-- features/comment-list/ui/CommentList.vue -->
<template>
<ul>
<li v-for="comment in comments" :key="comment.id">
<slot name="avatar" :userId="comment.userId" />
<span>{{ comment.text }}</span>
</li>
</ul>
</template>
<!-- pages/PostPage.vue -->
<template>
<CommentList :comments="comments">
<template #avatar="{ userId }">
<UserAvatar :userId="userId" />
</template>
</CommentList>
</template>
```
### Strategy D: Cross-feature reuse only via Public API
If strategies A-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-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/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/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?
│ ├─ Can boundaries be merged into one entity?
│ │ └─ YES → Merge. Stop.
│ └─ Boundaries must stay separate?
│ └─ 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 is almost
always a sign that the slices should be merged.
## 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.
@@ -0,0 +1,287 @@
# 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.
## 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, requiring careful design 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, or 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 (after the same logic is needed in 2+ places):
entities/profile/
model/
profile-validation.ts ← extracted only after reuse is real
```
### 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 keeping data definitions in `shared/api`.
```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`. Business logic that
operates on it (calculating discounts, applying promotions) lives in
`entities/order/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. Place CRUD in `shared/api`:
```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 is genuinely business logic. If so, the `entities`
layer may be appropriate. If not, keep it in `shared/api`.
### 4. Store authentication data in shared
Prefer `shared` over creating a `user` entity for auth tokens and session
DTOs. These are context-specific to authentication and unlikely to be
reused outside that scope. Wrapping a login response in a `user` entity
also tends to drag entities into cross-layer imports or `@x` chains,
complicating the architecture.
The Auth guide also documents **In Entities** (a `user` entity) as a
valid placement when the project already has an entities layer and the
data is genuinely reused. **In Pages/Widgets** is discouraged for both
guides.
**`shared/auth` (or `shared/api`) is the recommended default.** Choose
it when:
- The project has no entities layer yet
- Auth state is just a token plus minimal user info (id, email, role)
- Token management logic (refresh, expiration) is the main concern, not
user profile data
```text
shared/
auth/
use-auth.ts ← Token + minimal user info
index.ts
api/
client.ts ← API client reads token from shared/auth
endpoints/
order.ts
index.ts
```
This approach pairs naturally with an API client middleware that injects
the token into authenticated requests.
**A `user` entity is the right call when:**
- The project already has an entities layer
- Auth and profile data are tightly coupled (current user info is reused
across pages for non-auth purposes like comments, posts, mentions)
- Token management has complex business logic (invalidation policies,
multi-device session tracking) that benefits from co-location with the
user model
```text
entities/
user/
model/
current-user.ts ← Token + full user model + business logic
user.ts ← Generic user type, used for other users too
api/
get-current-user.ts
index.ts
```
When using the entity approach, the API client (in `shared/api`) needs
access to the token without violating the import rule. The official Auth
guide describes three solutions: pass the token manually on each request,
expose it through a context or `localStorage` with the key kept in
`shared/api`, or inject the token into the API client whenever the entity
store updates.
**Pages and widgets are discouraged.** Avoid placing the token store in a
page's `model/` segment or in a widget. App-wide state belongs in Shared
or Entities, not in route-bound or block-bound layers.
### Decision summary
| Project state | Recommended location |
| --- | --- |
| No entities layer (yet), simple token + minimal user info | `shared/auth` |
| Entities layer exists, auth and profile tightly coupled | `entities/user` |
| Complex token logic, no profile reuse yet | `shared/auth` (split from `shared/api`) |
| Token storage in a single page or widget | Avoid; promote to Shared or Entities |
A `user` entity created **only** to wrap a login response is premature.
Wait until profile data is consumed for non-auth purposes (avatars in
comments, names in posts) before introducing the entity.
### 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. No `@x`, no cross-imports,
no circular dependency risk.
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 business logic needs a home.
├─ Is the project a thin client?
│ └─ YES → Skip entities. Place in shared/ + page model.
├─ 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?
│ └─ YES → shared/api/endpoints/<resource>.ts
├─ Is it auth data (tokens, session, login DTOs)?
│ ├─ Project has no entities layer yet?
│ │ └─ YES → shared/auth/
│ ├─ Auth and profile data tightly coupled, entities layer exists?
│ │ └─ YES → entities/user/
│ └─ Otherwise → shared/auth/ (default).
│ Avoid placing in a page or widget.
├─ Is it just a TypeScript type for an API response?
│ └─ YES → shared/api/. No entity needed for types alone.
└─ Is it reusable domain logic confirmed in 2+ 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 confirmed reuse in 2+
consumers, not anticipated reuse.
- **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 profile data is reused across non-auth flows
(avatars in comments, names in posts) or when token logic is genuinely
tied to user business logic. Until that reuse appears, `shared/auth`
is simpler. Storing tokens in a page or widget is discouraged regardless
of the project shape.
- **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. CRUD goes
in `shared/api/endpoints/`.
## 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.
@@ -0,0 +1,496 @@
# 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/`).
All FSD projects follow the same `@/<layer>/*` path alias convention. The
exact configuration differs by framework. See each framework section
below. Astro is the one exception, using a single `@/*` alias instead.
## Next.js
FSD is compatible with both the App Router and the Pages Router. The main
conflict is that Next.js owns the `app/` and `pages/` folder names, and both
collide with FSD layer names. Resolve the conflict by moving the Next.js
routing folders to the project root and importing FSD pages from `src/` into
them.
### App Router
The Next.js `app/` folder lives at the project root. **Always create an
empty `pages/` folder at the project root as well, even when you only use
the App Router.** Without it, Next.js tries to use `src/pages/` as the Pages
Router and the build breaks. Add a `pages/README.md` explaining why the
folder exists.
#### 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
pages/ ← Empty, required even for App Router
README.md
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/
ui/HomePage.tsx
index.ts
profile/
ui/ProfilePage.tsx
model/profile.ts
api/fetch-profile.ts
index.ts
widgets/ ← FSD widgets layer (when needed)
features/ ← FSD features layer (when needed)
entities/ ← FSD entities layer (when needed)
shared/ ← FSD shared layer
ui/
lib/
api/
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';
```
Always re-export both the component and `metadata`. Route files contain no logic.
### Pages Router
The Pages Router uses `pages/` at the project root. The FSD `src/` tree is
unchanged. Routes re-export from `src/pages/`:
```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 component itself lives in `src/app/custom-app/` and exports
`App` from its public API like any other FSD slice.
### Middleware and instrumentation
`middleware.js` and `instrumentation.js` must live at the **project root**,
next to the Next.js `app/` and `pages/` folders. Next.js will not detect
them inside `src/`.
### 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;
// app/api/example.ts
import { getExampleData } from '@/app/api-routes';
export const config = getExampleData.config;
export default getExampleData.handler;
```
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.
### Path aliases
```json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/app/*": ["src/app/*"],
"@/pages/*": ["src/pages/*"],
"@/widgets/*": ["src/widgets/*"],
"@/features/*": ["src/features/*"],
"@/entities/*": ["src/entities/*"],
"@/shared/*": ["src/shared/*"]
}
}
}
```
Next.js reads `tsconfig.json` paths automatically. No `next.config.js`
alias configuration is needed.
### Server Components and Public API splitting
FSD layers work inside both Server and Client Components. However, the
standard single `index.ts` public API can cause problems in RSC environments
because re-exporting client and server code from the same entry point may
trigger bundler errors or unintended boundary crossings.
Split the public API into multiple entry points per environment:
```text
entities/user/
model/
user.ts
ui/
UserAvatar.tsx ← 'use client', uses hooks
UserProfileCard.tsx ← Server Component, no hooks
api/
user-queries.server.ts ← Server-only data fetching
index.ts ← Shared exports (types, pure functions)
index.client.ts ← Client component exports
index.server.ts ← Server component + server-only exports
```
```typescript
// entities/user/index.ts: shared (types, pure logic, no components)
export type { User } from "./model/user";
export { formatUserName } from "./model/user";
// entities/user/index.client.ts: client components only
export { UserAvatar } from "./ui/UserAvatar";
// entities/user/index.server.ts: server components + server-only code
export { UserProfileCard } from "./ui/UserProfileCard";
export { fetchUser } from "./api/user-queries.server";
```
```typescript
// Consumers import from the appropriate entry point:
// In a Server Component (pages/profile/ui/ProfilePage.tsx)
import { UserProfileCard } from "@/entities/user/index.server";
import type { User } from "@/entities/user";
// In a Client Component (features/comment/ui/CommentAuthor.tsx)
import { UserAvatar } from "@/entities/user/index.client";
```
**Rules for split public APIs:**
1. **`index.ts`**: types, constants, and pure functions that work in both
environments. Default import path.
2. **`index.client.ts`**: components using `'use client'`, hooks, or
browser APIs.
3. **`index.server.ts`**: Server Components and server-only data fetching.
4. The `index.[env].ts` pattern generalises beyond RSC. Any meta-framework
with distinct runtime environments can use it (e.g., `index.edge.ts`).
Verified for Next.js App Router; Nuxt and Astro compatibility is under
review.
5. Steiger support for multiple entry points is available or coming in an
upcoming release. If Steiger flags these files, check for version
updates.
**When NOT to split:** A slice with no client/server boundary concerns uses
a single `index.ts`. Split only when a slice actually has both client and
server exports.
## Nuxt 3
### Directory structure
```text
my-nuxt-project/
pages/ ← Nuxt file-based routing
index.vue ← Route entry, imports from FSD pages layer
profile.vue
src/
app/ ← FSD app layer
providers/
pages/ ← FSD pages layer
home/
ui/HomePage.vue
index.ts
profile/
ui/ProfilePage.vue
model/profile.ts
index.ts
shared/ ← FSD shared layer
ui/
lib/
api/
```
### Wiring Nuxt routes to FSD pages
```vue
<!-- pages/index.vue: thin route entry -->
<template>
<HomePage />
</template>
<script setup>
import { HomePage } from "@/pages/home";
</script>
```
### Path aliases
In addition to the standard `tsconfig.json` mapping, Nuxt requires explicit
runtime aliases in `nuxt.config.ts`:
```typescript
// nuxt.config.ts
import { resolve } from "path";
export default defineNuxtConfig({
alias: {
"@/app": resolve(__dirname, "src/app"),
"@/pages": resolve(__dirname, "src/pages"),
"@/widgets": resolve(__dirname, "src/widgets"),
"@/features": resolve(__dirname, "src/features"),
"@/entities": resolve(__dirname, "src/entities"),
"@/shared": resolve(__dirname, "src/shared"),
},
});
```
## 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"),
"@/widgets": resolve(__dirname, "src/widgets"),
"@/features": resolve(__dirname, "src/features"),
"@/entities": resolve(__dirname, "src/entities"),
"@/shared": resolve(__dirname, "src/shared"),
},
},
});
```
## Create React App (CRA)
CRA is no longer actively maintained. **Migrate to Vite for new projects.**
If you must stay on CRA, path aliases require ejecting or using `craco` to
override the webpack config:
```javascript
// craco.config.js
const path = require("path");
module.exports = {
webpack: {
alias: {
"@/app": path.resolve(__dirname, "src/app"),
"@/pages": path.resolve(__dirname, "src/pages"),
"@/widgets": path.resolve(__dirname, "src/widgets"),
"@/features": path.resolve(__dirname, "src/features"),
"@/entities": path.resolve(__dirname, "src/entities"),
"@/shared": path.resolve(__dirname, "src/shared"),
},
},
};
```
## 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
widgets/
features/
entities/
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)
Astro projects use a single `@/*` alias instead of one alias per layer. This
is the convention the FSD Astro guide recommends:
```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.
## Key Reminders for All Frameworks
1. **FSD lives in `src/`**: root-level `app/` and `pages/` belong to the
framework's routing, not FSD.
2. **Framework route files are thin wrappers**: they import and render FSD
page components. Business logic stays in FSD pages.
3. **Path aliases are required**: configure both the bundler and
`tsconfig.json`.
4. **Pages First still applies**: regardless of framework, start with code
in FSD `pages/` and extract only when needed.
@@ -0,0 +1,490 @@
# 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 currently being reused across multiple
pages with stable boundaries (extract to a lower layer when reuse is
confirmed, not anticipated).
### Page Layout Patterns
A typical page composes widgets, features, and entities from lower layers,
plus its own local UI components:
```typescript
// pages/product-detail/ui/ProductDetailPage.tsx
import { Header } from '@/widgets/header';
import { AddToCart } from '@/features/add-to-cart';
import { Product } from '@/entities/product';
export const ProductDetailPage = ({ productId }) => {
const product = useProductDetail(productId); // local hook in this page
return (
<>
<Header />
<Product.Card 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
Composite UI blocks with their own logic, **reused across multiple pages**.
Add this layer only when UI blocks actually appear in 2+ pages and sharing
provides clear value.
```text
widgets/
header/
ui/
Header.tsx
Navigation.tsx
UserMenu.tsx
model/
header.ts ← Widget state
api/
fetch-notifications.ts
index.ts
sidebar/
ui/
Sidebar.tsx
model/
sidebar.ts
index.ts
```
**Belongs in widgets:** Navigation bars, sidebars, dashboards, footers,
complex card layouts that combine data from multiple entities/features.
**Does not belong:** Simple UI primitives (→ `shared/ui/`), single-use
page sections (→ keep in the page).
---
## Features Layer
Independent, reusable user interactions. **Create only when used in 2+ places.**
```text
features/
auth/
ui/
LoginForm.tsx
RegisterForm.tsx
model/
auth.ts ← Auth state + logic
api/
login.ts
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
// widgets/post-card/ui/PostCard.tsx
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 only when used in 2+ places. 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)
// ⚠️ 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, widgets,
// pages), 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
assets/ ← Branding assets shared across the app (use sparingly)
```
```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, common types). It must **never** contain business logic,
feature-specific code, or entity-specific code.
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 the business domain, not the technical
role:
```text
// ❌ Technical-role naming: mixes domains
model/types.ts ← Which types? User? Order?
model/utils.ts
api/endpoints.ts
model/selectors.ts
// ✅ 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
```
### 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.
- Only two or three slices would end up in the group.
### 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 two or three slices is not the
entire content.
### 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. Extract reusable
code to `shared/` instead. 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.** If two slices in the
same group need to share code, extract it one layer down rather than
adding a `_common/` file.
---
## Path Aliases
Configure path aliases so imports follow the `@/layer/slice` pattern:
```json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/app/*": ["src/app/*"],
"@/pages/*": ["src/pages/*"],
"@/widgets/*": ["src/widgets/*"],
"@/features/*": ["src/features/*"],
"@/entities/*": ["src/entities/*"],
"@/shared/*": ["src/shared/*"]
}
}
}
```
For framework-specific alias configuration (Vite, Next.js, Nuxt, Astro),
see `references/framework-integration.md`.
@@ -0,0 +1,293 @@
# 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.
## 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. If reuse is needed across several pages,
move it to a 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`**: an entity or feature used by only one page.
This rule will suggest merging that entity or feature into the page
entirely.
- **`excessive-slicing`**: too many slices in a single layer.
For each flagged slice, decide:
- Reused in 2+ places → keep in features/entities.
- Used only in one page → mark for migration back into that page.
### Step 2. Move single-use code back to its consumer
Take single-use features and entities and inline them into the consuming
page (or widget if that is the single consumer):
```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
Code confirmed to be used in 2+ places 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 multiple
pages share workflow state, create a feature for it.
- **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 ← Used in 2+ pages
app/sync/background-sync.ts ← Global concern
```
### Post-migration verification
1. Run `npx steiger src`. All `insignificant-slice` warnings should be gone.
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.
## 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
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/ ← only objects that are reused
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, and selectors become the `model` segment.
- Thunks and mutations become the `api` segment.
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
Reused Redux slices typically describe business concepts (products, users)
or user actions (comments, likes):
- Business entities → **Entities layer**, one entity per folder.
- User actions → **Features layer**.
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
The `modules/` folder typically holds business logic, similar in nature to
the Features layer. Some modules describe large UI chunks (an app header)
which belong in the Widgets layer.
### 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. If business logic is not used in many
places, copy-pasting back to consumers 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/`, `entities/`, or
`widgets/` directories until there is content for them.
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).
@@ -0,0 +1,533 @@
# Practical Examples
Concrete code patterns for common scenarios within FSD structure. Covers
authentication, type definitions, API request handling, and state management
integration (Redux, TanStack Query / React Query).
## 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/`
Tokens, session state, and login utilities are **infrastructure**, not
business logic. Keep them in shared:
```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.
### 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
ui/{LoginForm,RegisterForm}.tsx
model/auth.ts
api/{login,register}.ts
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).
Pages and widgets are discouraged.
`shared/auth` is the simpler default. Choose it when the project has no
entities layer yet, or when auth state is just a token plus minimal user info.
A `user` entity is the right call when the project already has an
entities layer **and** auth and profile data are tightly coupled (profile
reused for non-auth purposes like avatars in comments).
```text
// Path A: shared/auth (simpler default)
shared/auth/session.ts ← userId, email, role, token
// Path B: user entity (entities layer exists, profile reuse is real)
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.
See `references/excessive-entities.md` for the full decision matrix.
## 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>`) | Domain-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: domain model layered on top
import type { ProductDTO } from "@/shared/api/product";
export interface Product extends ProductDTO {
formattedPrice: string;
isOnSale: boolean;
}
export const fromDTO = (dto: ProductDTO): Product => ({
...dto,
formattedPrice: `$${dto.price.toFixed(2)}`,
isOnSale: dto.price < 10,
});
```
**Key principle:** Raw API shapes go in `shared/api/`. Domain models with
business logic go in `entities/`. 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 } from "@/shared/api/client";
import type { ProductDTO } from "@/shared/api/product";
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/token";
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;
});
```
### 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
Place each request function in the slice that owns the use case:
- **Page-specific data fetching** (e.g., dashboard stats only used on the
dashboard) → `pages/<name>/api/`
- **Feature-specific actions** (e.g., `toggleLike`) → `features/<name>/api/`
- **Reusable domain queries** (e.g., `getUserById`) → `entities/<name>/api/`
- **CRUD primitives** for a generic resource → `shared/api/create-crud-api.ts`
Do not put domain-specific request functions in `shared/api/`. Shared is
infrastructure; the moment a function knows about a specific resource and
its domain rules, it belongs in `entities/` or higher.
## State Management: Redux
### Where a Redux slice belongs
The `from-custom` migration guide draws a clean line: **business
entities** (the things your app works with, like `todo`, `product`, `user`)
go in the Entities layer; **user actions** (`add-todo`, `toggle-todo`,
`like-post`) go in Features.
In v2.1, also remember the pages-first rule: if the slice is used by a
single page, keep it in that page's `model/` segment until reuse appears.
### Business-entity slice in entities
```typescript
// entities/todo/model/todo.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { apiClient } from "@/shared/api/client";
interface Todo { id: string; title: string; completed: boolean }
interface TodoState { items: Todo[]; loading: boolean }
export const fetchTodos = createAsyncThunk("todos/fetch", async () =>
(await apiClient.get<Todo[]>("/todos")).data,
);
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: RootState) => state.todos.items;
export const todoReducer = todoSlice.reducer;
```
The slice's public API re-exports what consumers need:
```typescript
// entities/todo/index.ts
export { todoReducer, selectTodos, setCompleted, fetchTodos } from "./model/todo";
```
**Key:** The entire Redux slice (reducer + selectors + thunks) lives in a
single domain-named file, not split across `reducers.ts`, `selectors.ts`,
`thunks.ts`. That technical-role split reduces cohesion and is an
anti-pattern in FSD.
### User-action slice in features
A user action that orchestrates the entity exposes a hook through its
public API and consumes the entity's reducer:
```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)
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 based on project size and whether the
project already has an Entities layer.
**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 each request
corresponds to a single entity, and the project already has an Entities
layer. When entities reference each other, see
`references/cross-import-patterns.md` for `@x` notation as a last resort.
### Where to store mutations
Do not mix mutations with queries. 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:** all API requests for a domain live in one
place (readability), every key and query function is reachable through the
same object (convenient access), and refetching is a one-line call
(`queryClient.invalidateQueries({ queryKey: POST_QUERIES.all() })`) without
hunting down keys across the codebase.
### 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/widgets/save-indicator/ui/save-indicator.tsx
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
Tools that generate clients from an OpenAPI/Swagger spec are less flexible
than hand-written factories. If your spec is clean and you adopt a generator,
place the generated code in `@/shared/api/`.
### 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 in the slice that owns the
domain. Page-specific queries stay in the page. Shared queries go in
`shared/api/` or `entities/<name>/api/` depending on whether the project has
an Entities layer.
## See also
- [Sample project on GitHub](https://github.com/ruslan4432013/fsd-react-query-example)
- [Query options API (tkdodo blog)](https://tkdodo.eu/blog/the-query-options-api)