Files
main_frontend/.agents/skills/feature-sliced-design/references/migration-guide.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

10 KiB

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:

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

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

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

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.

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.

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. The v1 → v2 transition introduced the entities and processes layers (processes was later deprecated in v2.1).