491 lines
14 KiB
Markdown
491 lines
14 KiB
Markdown
# 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`.
|