Files
main_frontend/.agents/skills/feature-sliced-design/references/layer-structure.md
T
Daniil 21e936a827
dev / deploy (push) Successful in 2m15s
compute / deploy (push) Has been cancelled
chore: agentic upgrade
2026-05-17 02:11:33 +03:00

14 KiB

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):

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:

app/
  router.tsx       ← Route configuration
  store.ts         ← Store configuration
  styles/
    global.css
  providers.tsx    ← All providers in one file
  index.tsx        ← Entry point
// 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.

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:

// 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):

// 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.

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.

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:

// 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.

// 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.

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)
// 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:

// ❌ 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:

// ❌ 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:

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:

// 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.
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:

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:

// 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.