Skip to content

Feature-Sliced Design

Markdown for agents

This guide checks a front end that uses Feature-Sliced Design (FSD) with the fsd preset. The preset holds the rules of Steiger, the official linter for FSD.

  • Read Getting started.
  • Your front end has the FSD layers in one folder: app, pages, widgets, features, entities and shared. A repository needs only some of them.

The example is a small store front. An import between two slices uses the alias @/, which tsconfig.json points at src:

  • archangel.config.ts
  • tsconfig.json
  • Directorysrc/
    • Directoryapp/
      • index.ts
    • Directorypages/
      • Directoryhome/
        • index.ts
        • Directoryui/
          • home-page.ts
    • Directoryfeatures/
      • Directoryadd-to-cart/
        • index.ts
        • Directorymodel/
          • cart.ts
        • Directoryui/
          • add-to-cart-button.ts
    • Directoryentities/
      • Directoryproduct/
        • index.ts
        • Directorymodel/
          • product.ts
        • Directoryui/
          • product-card.ts
    • Directoryshared/
      • Directoryui/
        • button.ts
        • index.ts
  1. Put the preset in extends. root is the folder that holds the layers:

    archangel.config.ts
    import { defineConfig, fsd } from '@enhance-eng/archangel';
    export default defineConfig({
    extends: [fsd({ root: 'src' })],
    });

    The config has no parts and no rules of its own. The preset declares each layer as a part, and each slice as an instance.

  2. Check the repository:

    Terminal
    $ npx archangel check
    error[plugin]: This slice has only one reference in slice "pages/home". Consider merging them.
    --> src/entities/product
    = note: rule: fsd/insignificant-slice
    = note: reason: A slice that at most one other slice uses belongs in that slice.
    = note: see: https://feature-sliced.github.io/steiger/rules/insignificant-slice
    error[may-not-import]: src/entities/product/ui/product-card.ts may not import "@/features/add-to-cart", because files in "entities" may not import "fsd/features", "fsd/widgets", "fsd/pages", "fsd/app"
    --> src/entities/product/ui/product-card.ts:1:33
    |
    1 | import { addToCartButton } from '@/features/add-to-cart';
    | ^^^^^^^^^^^^^^^^^^^^^^^^ remove the import, or move the code to files that may import it
    |
    = note: rule: fsd/forbidden-imports
    = note: reason: A module may import only from layers below its own, and a slice not from another slice of its layer, except through @x.
    = note: see: https://feature-sliced.github.io/steiger/rules/forbidden-imports
    checked 12 files and 13 imports: 2 problems
    fsd/widgets matches nothing in this repository
    fsd/processes matches nothing in this repository
    For what a code means, run `archangel docs plugin` or `archangel docs may-not-import`.

    The check finds two problems:

    • fsd/forbidden-imports: the entity product imports the feature add-to-cart. In FSD, a layer imports only from the layers below it.
    • fsd/insignificant-slice: only one other slice uses the entity product. Steiger tells you to merge it into that slice.

    The summary also lists the preset parts that find no file, here widgets and processes. This is not an error. A preset declares each layer of FSD, and most repositories have only some of them.

  3. Run the examples:

    Output of archangel test
    Terminal
    $ npx archangel test
    pass fsd/forbidden-imports accepted #1 src/entities/example-user/index.ts, src/features/example-login/model/a.ts
    pass fsd/forbidden-imports accepted #2 src/entities/example-a/model/a.ts, src/entities/example-b/@x/example-a.ts
    pass fsd/forbidden-imports rejected #1 src/features/example-login/index.ts, src/shared/example-lib/index.ts
    pass fsd/forbidden-imports rejected #2 src/features/example-a/model/a.ts, src/features/example-b/index.ts
    pass fsd/no-public-api-sidestep accepted #1 src/features/example-login/index.ts, src/pages/example-home/ui/a.ts
    pass fsd/no-public-api-sidestep rejected #1 src/features/example-login/model/x.ts, src/pages/example-home/ui/a.ts
    pass fsd/no-public-api-sidestep rejected #2 src/features/example-login/model/a.ts, src/shared/example-api/client.ts, src/shared/example-api/index.ts
    pass fsd/public-api accepted #1 src/features/example-login/index.ts, src/features/example-login/model/x.ts
    pass fsd/public-api rejected #1 src/features/example-login/model/x.ts
    pass fsd/no-layer-public-api rejected #1 src/entities/index.ts
    pass fsd/no-reserved-folder-names rejected #1 src/shared/example-kit/ui/button.ts
    pass fsd/no-segmentless-slices accepted #1 src/features/example-login/index.ts, src/features/example-login/model/login.ts
    pass fsd/no-segmentless-slices rejected #1 src/features/example-login/index.ts, src/features/example-login/login.ts
    pass fsd/no-segments-on-sliced-layers rejected #1 src/features/ui/button.ts
    pass fsd/no-ui-in-app rejected #1 src/app/ui/root.tsx
    pass fsd/no-ui-in-app rejected #2 src/app/ui.tsx
    pass fsd/segments-by-purpose rejected #1 src/features/example-login/utils/format.ts
    pass fsd/segments-by-purpose rejected #2 src/shared/helpers.ts
    pass fsd/no-processes rejected #1 src/processes/example-checkout/index.ts
    pass fsd/typo-in-layer-name accepted #1 src/features/example-login/index.ts
    pass fsd/typo-in-layer-name rejected #1 src/featurez/example.ts
    pass fsd/ambiguous-slice-names accepted #1 src/features/example-login/model/a.ts
    pass fsd/ambiguous-slice-names rejected #1 src/features/example-kit/model/a.ts, src/shared/example-kit/index.ts
    pass fsd/inconsistent-naming accepted #1 src/entities/example-group/apples/model/a.ts, src/entities/example-group/pears/model/a.ts
    pass fsd/inconsistent-naming rejected #1 src/entities/example-group/order-line/model/a.ts, src/entities/example-group/orders/model/a.ts
    pass fsd/insignificant-slice accepted #1 src/entities/example-used/index.ts, src/entities/example-used/model/a.ts, src/pages/example-one/ui/a.ts, src/pages/example-two/ui/b.ts
    pass fsd/insignificant-slice rejected #1 src/entities/example-lonely/index.ts, src/entities/example-lonely/model/a.ts
    pass fsd/repetitive-naming accepted #1 src/features/example-group/drop/model/a.ts, src/features/example-group/load/model/a.ts, src/features/example-group/save/model/a.ts
    pass fsd/repetitive-naming rejected #1 src/features/example-group/item-drop/model/a.ts, src/features/example-group/item-load/model/a.ts, src/features/example-group/item-save/model/a.ts
    pass fsd/excessive-slicing accepted #1 src/widgets/example-group/footer/ui/a.ts, src/widgets/example-group/header/ui/a.ts
    pass fsd/excessive-slicing rejected #1 src/widgets/example-group/w01/ui/a.ts, src/widgets/example-group/w02/ui/a.ts, src/widgets/example-group/w03/ui/a.ts, src/widgets/example-group/w04/ui/a.ts, src/widgets/example-group/w05/ui/a.ts, src/widgets/example-group/w06/ui/a.ts, src/widgets/example-group/w07/ui/a.ts, src/widgets/example-group/w08/ui/a.ts, src/widgets/example-group/w09/ui/a.ts, src/widgets/example-group/w10/ui/a.ts, src/widgets/example-group/w11/ui/a.ts, src/widgets/example-group/w12/ui/a.ts, src/widgets/example-group/w13/ui/a.ts, src/widgets/example-group/w14/ui/a.ts, src/widgets/example-group/w15/ui/a.ts, src/widgets/example-group/w16/ui/a.ts, src/widgets/example-group/w17/ui/a.ts, src/widgets/example-group/w18/ui/a.ts, src/widgets/example-group/w19/ui/a.ts, src/widgets/example-group/w20/ui/a.ts, src/widgets/example-group/w21/ui/a.ts
    pass fsd/shared-lib-grouping accepted #1 src/shared/lib/example-dates/index.ts
    pass fsd/shared-lib-grouping rejected #1 src/shared/lib/example-01.ts, src/shared/lib/example-02.ts, src/shared/lib/example-03.ts, src/shared/lib/example-04.ts, src/shared/lib/example-05.ts, src/shared/lib/example-06.ts, src/shared/lib/example-07.ts, src/shared/lib/example-08.ts, src/shared/lib/example-09.ts, src/shared/lib/example-10.ts, src/shared/lib/example-11.ts, src/shared/lib/example-12.ts, src/shared/lib/example-13.ts, src/shared/lib/example-14.ts, src/shared/lib/example-15.ts, src/shared/lib/example-16.ts
    33 examples, none failed; 0 rules without a rejected example

    Each rule of the preset has examples. Archangel checks them in your repository, under root, with your tsconfig.json. So a passing test also tells you that root is correct.

  1. Remove the import of the feature from the entity. An entity shows a product. It does not add it to a cart:

    src/entities/product/ui/product-card.ts
    import { button } from '@/shared/ui';
    export function productCard(name: string) {
    return button(name, () => {});
    }
  2. Check again:

    Terminal
    $ npx archangel check
    error[plugin]: This slice has only one reference in slice "pages/home". Consider merging them.
    --> src/entities/product
    = note: rule: fsd/insignificant-slice
    = note: reason: A slice that at most one other slice uses belongs in that slice.
    = note: see: https://feature-sliced.github.io/steiger/rules/insignificant-slice
    error[plugin]: This slice has only one reference in slice "pages/home". Consider merging them.
    --> src/features/add-to-cart
    = note: rule: fsd/insignificant-slice
    = note: reason: A slice that at most one other slice uses belongs in that slice.
    = note: see: https://feature-sliced.github.io/steiger/rules/insignificant-slice
    checked 12 files and 12 imports: 2 problems
    fsd/widgets matches nothing in this repository
    fsd/processes matches nothing in this repository
    For what a code means, run `archangel docs plugin`.

    Now no entity imports add-to-cart, so only the page uses each slice. fsd/insignificant-slice finds both slices.

A store front this small has slices that only one page uses. The team decides that this is correct. It also wants the four rules of Steiger that are off by default.

  1. Add strict: true and put the rule name in off:

    archangel.config.ts
    import { defineConfig, fsd } from '@enhance-eng/archangel';
    export default defineConfig({
    extends: [fsd({ root: 'src', strict: true })],
    off: ['fsd/insignificant-slice'],
    });

    strict: true adds the rules fsd/no-cross-imports, fsd/no-higher-level-imports, fsd/import-locality and fsd/no-wildcard-exports.

  2. Check again:

    Terminal
    $ npx archangel check
    checked 12 files and 12 imports: no problems
    fsd/widgets matches nothing in this repository
    fsd/processes matches nothing in this repository

You can also change one rule of the preset:

  • Remove a rule. Put its name in off, as above. archangel rules lists the rules that off removes.
  • Replace a rule. Write a rule with the same name, for example fsd/public-api. Your rule replaces the rule of the preset.
  • A typing error stops the check. A name in off, and the name of a replacement rule, must match a rule of the preset. If it does not, archangel stops and shows the nearest correct name.

To use the preset two times, give the second use its own name, for example fsd({ root: 'apps/admin/src', name: 'fsd-admin' }). Each name in a preset starts with the preset’s name, so the rules of the second use are fsd-admin/forbidden-imports and so on.

  • Archangel finds a slice by its folder. Each folder in a sliced layer is a slice. Steiger finds slices by the segments in them.
  • Each rule has examples, and archangel test runs them.
  • If all parts of the preset find no file, the root is probably wrong. Archangel shows preset-empty.

Read Migrate from Steiger to move an existing Steiger setup.