Skip to main content

FSD: where the code lives when the components folder no longer holds

Constantin Potapov
7 min

Feature-Sliced Design layers, the top-down import rule, and where I cut the frame. After the first large frontend I put it almost everywhere: this site, Slot-Me, even a small PassWave.

FSD: where the code lives when the components folder no longer holds

Every project starts with a clean folder and a fresh create-app. Six months later UserCard depends on ProductList, the build dies on circular imports, and the new SuperButton has nowhere to live: too fat for the UI kit, too shared for one page.

The question is not pretty folders. The question is where this code lives so that in three months the person who did not write it does not have to guess. The components folder is the first to lie. A button, a user card, a slice of checkout and a “temporary” hook from last quarter sit there as equals. They are not equals.

Since I moved the first large frontend to Feature-Sliced Design, I put it almost everywhere. This site is layered shared → entities → features → widgets → app. The Slot-Me frontend too. Even a small PassWave kept FSD so two weeks of work would not grow a dump. The LeadForge frontend is the same shape. After a dozen such layouts the question “where does this go” stops being taste and becomes a habit, like putting keys on the same shelf.

Layers

  1. shared: toolbox. UI primitives, utilities, clients. It knows neither the user nor the order.
  2. entities: domain objects. User, Product, Order. The model and a thin piece of UI for that model.
  3. features: user scenarios. Sign in, add to cart, toggle theme.
  4. widgets: large blocks of features and entities. Header, product grid.
  5. pages: the page as a whole.
  6. app: providers, routing, theme.

Golden rule: import top-down. A page may take a widget. A widget may not import a page. Reverse import is a red card.

On this site you cannot reach into a file across layers. Only through the public index.ts. ESLint blocks @/shared/ui/button and allows @/shared/ui. Otherwise half the project starts dating the internals of a button. Then you cannot move the button without waking the cart.

SuperButton is either a primitive in shared/ui, if it knows nothing about the domain, or a piece of a feature if it can “place an order”. There is no third home, and that is the point. Nowhere to hide a demigod.

What this does with your hands

A task breaks into “where to put it”. Primitive in shared/ui, entity card in entities, action in features, composition in widgets. You can still argue on review whether something is a hook or a feature. You no longer argue whether a folder for it exists.

Lower layers do not know about upper ones. A button from shared does not drag the cart and the router with it. It is a boring rule. It saves more nerves than any manifesto about scale.

A newcomer understands the layout in a day. Not because FSD is magic, because the map is one. Ownership is simpler too: a feature sits in one place, not smeared across components, hooks and utils/helpers2.

Circular imports drop not by magic, but because reverse import is simply forbidden. If you really want it, lint shouts now, not a month later in production.

Onboarding speeds up not because the architecture is “modern”. A newcomer opens the tree and sees the same map they saw yesterday in another of my repos. Review is shorter too: the fight “is this a hook or a util” stays, the fight “do we create another misc folder” dies.

Usual traps

Shoving everything into features. In a month that folder is a dump. Data and the entity model go to entities. What lives without business goes to shared.

Business logic in shared. Then the “screwdriver” suddenly knows how to compute a wholesale discount. shared should stay sterile. Otherwise the button imports the order, and two files later the order imports the button.

Layers without rules. If nobody watches imports, FSD is decoration over the same components folder. Lint plus an agreement on what counts as a violation. Literalness for its own sake is useless. Discipline is not.

Mini-FSD

On a small project you do not need a six-floor parade. shared → features → pages is enough. When you outgrow it, add entities and widgets. On a two-screen landing, a week-long prototype, or a short solo cycle the frame often costs more than it returns. There it is enough not to lie to yourself with folders.

FSD is not a religion. It is an answer to “where does this go” after the components folder has started lying.

See also: Slot-Me, PassWave.