All writing
9 min read

Feature-Sliced Design: Taming a Growing React Codebase

Folder structure is architecture. How layered, feature-first organization with enforced import rules keeps a large frontend from collapsing into spaghetti.

ArchitectureReactMaintainability

Every large frontend I have worked on eventually hits the same wall: nobody can change anything without touching everything. A 'small' edit ripples across the codebase because imports go in every direction and there is no agreed-upon notion of what depends on what. Feature-Sliced Design (FSD) is the methodology that fixed this for me — not because it is clever, but because it makes the dependency direction explicit and enforceable.

Layers with a one-way dependency rule

FSD organizes code into a fixed set of layers, ordered by responsibility. The non-negotiable rule: a module may only import from layers below it, never above or sideways at the same level (with one deliberate exception). From top to bottom:

  • app — providers, routing, global styles; the composition root.
  • pages — route-level compositions that assemble widgets and features.
  • widgets — self-contained UI blocks like a header or a sidebar.
  • features — user-facing interactions, e.g. 'add to cart' or 'toggle subscription'.
  • entities — business domain models like user or product.
  • shared — framework-agnostic utilities, UI kit, and API clients with no business logic.

Because dependencies only ever point downward, you can reason about impact. A change in shared may affect everything above it, but a change in a feature can never break an entity. That single constraint is what keeps the graph acyclic and the codebase navigable.

Slices and the public API rule

Within a layer, code is divided into slices by business domain — user, cart, auth. Each slice exposes a public API through an index barrel, and everything else inside it is private. Outside code imports from the slice's entry point, never reaches into its internals:

ts
// entities/user/index.ts — the slice's public API
export { UserCard } from "./ui/UserCard";
export { useUser } from "./model/useUser";
export type { User } from "./model/types";

// ✅ allowed: import through the public API
import { UserCard, type User } from "@/entities/user";

// ❌ forbidden: reaching into private internals
import { UserCard } from "@/entities/user/ui/UserCard";

This is the same encapsulation principle that makes well-designed packages pleasant to use, applied inside your own app. The internal structure of a slice becomes free to refactor, because nothing outside depends on it.

Enforce it with tooling, not goodwill

Conventions that rely on people remembering them decay the moment a deadline looms. Encode the layering rules in the linter so violations fail CI. The import/no-restricted-paths rule or the dedicated eslint-plugin-boundaries make the architecture self-policing:

js
// eslint — forbid upward and illegal cross-layer imports
"import/no-restricted-paths": ["error", {
  zones: [
    // entities must never import from features or above
    { target: "./src/entities", from: "./src/features" },
    { target: "./src/entities", from: "./src/widgets" },
    { target: "./src/shared", from: "./src/entities" },
  ],
}]
An architecture that lives only in a wiki is a suggestion. An architecture encoded in the linter is a guarantee. Make the wrong thing impossible, not merely discouraged.

What it costs, and when to adopt it

FSD is not free. It adds indirection and a learning curve, and on a small app it is overkill — a few well-named folders will do. Reach for it when a codebase has crossed the threshold where multiple people step on each other, where 'where does this go?' is a recurring question, and where refactors feel dangerous because the blast radius is unknowable.

You do not have to adopt the methodology by the letter — I have shipped pragmatic variants on every team. What matters is the underlying idea: explicit layers, a one-way dependency rule, slices with public APIs, and tooling that enforces all three. Get those right and a large frontend stays as easy to change at year three as it was at month one.