Skip to content

    Frontend System Design

    Feature-Based and Modular Folder Structure

    Decide when to organise a front-end codebase by feature instead of file type, set a one-way dependency direction, and migrate without a big rewrite.

    By Published 12 min read

    • frontend-system-design
    • folder-structure
    • feature-folders
    • architecture
    Share

    Standard lessonbeginnerFrontend System Design

    Before this: Component-Based Architecture (coming soon)

    What you'll learn: By the end of this lesson you will be able to compare the two common ways to arrange a front-end codebase (by file type and by feature), explain why a folder layout is really a statement about which code may depend on which, lay out feature folders with a one-way dependency direction (shared, then features, then app), recognise the layered "slices" variant used by Feature-Sliced Design, and plan an incremental migration. Along the way you will see which parts are documented guidance and which are the opinion of particular projects.

    When you move house you can pack by room or by kind of object. Packing by kind puts every lamp in one box and every cable in another. It is neat, and on the first night you cannot find the one lamp, its plug and its bulb because they are in three boxes. Packing by room puts the bedroom things in a bedroom box. Things that are used together travel together.

    A front-end codebase has the same choice. A folder for all components, another for all hooks and another for all API calls is the "by kind" layout. A folder per product capability (cart, search, checkout) is the "by room" layout. The running example is a storefront with a product list, a search box, a cart and a checkout, built by a team of about a dozen developers (illustrative).

    The core idea

    1. Group by what changes together. A change request is usually "add a gift note to the cart", not "change all hooks". A layout where that change touches one folder is cheaper to work in and to review.
    2. A folder structure is a dependency statement. Where a file lives tells readers what it may import. If a layout does not say which folders may import which, it only moves the mess around.
    3. There is no single correct layout. React's own guidance is relaxed, as the next section shows. The case for feature folders is an argument from several projects' documentation, not a rule of the language or the library.

    What the official guidance says, and does not

    The React FAQ page on file structure (part of the older documentation site) says React "doesn't have opinions on how you put files into folders". It describes two popular approaches without choosing: grouping by features or routes (CSS, JavaScript and tests together, in a folder for each feature or route) and grouping by file type (separate folders for, say, API files and components). It adds that the definition of a feature "is not universal", so you choose the granularity. It warns that deep nesting makes relative imports hard to write and suggests limiting yourself to three or four nested folders unless there is a compelling reason. And it says not to spend more than five minutes on the choice for a new project, and to keep all files in one folder if you are stuck.

    Keep that last point in mind. A small app does not need an architecture. The structure below pays off as a team and a codebase grow, and what follows is an opinion with reasons attached, not a requirement.

    Two layouts, side by side

    Illustration 1 (minimal): the same six files two ways. Take a cart badge, a cart page, a product card, a hook that reads the cart, a function that calls the cart API and a product list page.

    By file type                         By feature
    src/                                 src/
      components/                          features/
        CartBadge.tsx                        cart/
        CartPage.tsx                           CartBadge.tsx
        ProductCard.tsx                        CartPage.tsx
      hooks/                                   useCart.ts
        useCart.ts                             cartApi.ts
      api/                                     index.ts
        cartApi.ts                         product-list/
      pages/                                   ProductCard.tsx
        ProductListPage.tsx                    ProductListPage.tsx
                                               index.ts

    Adding a gift note to the cart touches components/, hooks/ and api/ on the left and one folder on the right. Deleting the cart means finding its files across the tree on the left and deleting a folder on the right. The index.ts in each feature folder is its public API: the file other code is supposed to import. A planned companion lesson, Module Boundaries and Public APIs, covers making that rule enforceable.

    Figure 1. The same change in two layouts

    sequenceDiagram accTitle: Adding a gift note touches many folders or one accDescr: A developer adds a gift note to the cart. In a layout by file type the change spreads over the components, hooks and api folders and a reviewer must follow it. In a layout by feature it stays inside the cart folder. participant D as Developer participant T as By file type participant F as By feature D->>T: edit components/CartPage.tsx D->>T: edit hooks/useCart.ts D->>T: edit api/cartApi.ts Note over T: three folders, reviewer reads across them D->>F: edit features/cart/* Note over F: one folder, one place to review

    How to read this: the work is the same; what differs is how many places it lands in. This is the main practical argument for grouping by feature.

    What goes inside a feature folder

    The project bulletproof-react (a community reference repository, not an official React guide) recommends keeping most code in feature modules. Its project-structure page says each feature folder holds the code specific to that feature, and lists the folders a feature may contain: api, assets, components, hooks, stores, types and utils, adding that a feature includes only the folders it needs. The same page suggests that code shared across features sits in top-level folders (components, hooks, lib, types and utils), and that the application layer composes features.

    Illustration 2 (realistic): the storefront.

    src/
      app/                  routes, providers, the shell
      features/
        product-list/       components, hooks, api, index.ts
        search/
        cart/
        checkout/
      components/           shared UI: Button, Modal, Table
      lib/                  shared helpers: money, dates, http client

    Only what is specific to the cart lives in features/cart. A Button used by every feature lives in components/. The first rule of thumb that follows (an opinion, with the usual caveat): code earns a place in the shared folders when it is used by more than one feature and has no knowledge of any of them.

    Dependency direction

    The most important part of the layout is not the names but the arrows. Bulletproof-react's page describes a unidirectional codebase: code flows "from shared parts of the code to the application (shared, then features, then app)". Shared modules may be used anywhere; features may import only from shared; the app may import from features and shared. It also says it might not be a good idea to import across features, and that features should instead be composed at the application level. It enforces this with the ESLint rule import/no-restricted-paths using zones (it names this rule on the page).

    Figure 2. Allowed and forbidden imports

    App imports features, features import shared, and features never import each otherapp to cart (composes); app to search (composes); cart to shared (uses); search to shared (uses); cart to search (forbidden)FEATURESapp: routes, providersapproutes, providerscart: a featurecarta featuresearch: a featuresearcha featureshared: components, libsharedcomponents, libcomposescomposesusesusesforbidden

    How to read this: arrows point from the importer to the imported. The arrow labelled forbidden is the one the rule rejects; if two features need to talk, the app (or shared code) wires them.

    Layers and slices (Feature-Sliced Design). Feature-Sliced Design (FSD) is an architectural methodology with its own documentation, aimed at front-end applications. It formalises the same idea with more layers. Its overview lists seven layers from top to bottom (App, Processes which is deprecated, Pages, Widgets, Features, Entities and Shared) and one import rule: modules on a layer may only know about and import from layers strictly below. A layer is divided into slices by business domain, "slices cannot use other slices on the same layer", and a slice is divided into segments by purpose (ui, api, model, lib, config). FSD also says its approach suits applications rather than libraries, and that if the current architecture works, "maybe it's not worth changing".

    Treat FSD as a worked-out option, not the default. For many apps the three-part layout above is enough, and the extra layers are a cost until the codebase is big enough to need them.

    Choosing a layout

    Figure 3. Where does this file go?

    flowchart TD accTitle: Deciding where a new file belongs accDescr: A decision tree. If the file is used by only one feature, it goes in that feature. If several features use it and it knows nothing about them, it goes in shared. If it wires features together, it goes in the app layer. If it knows about several features but is not wiring, it is a sign a feature is too big or two features should merge. Q1{"Who uses
    this file?"} -->|"one feature"| A["Put it in that feature"] Q1 -->|"several features"| Q2{"Does it know about
    any one feature?"} Q2 -->|"no"| B["shared"] Q2 -->|"yes, wires them"| C["app layer"] Q2 -->|"yes, mixes their logic"| D["Rethink the boundaries"]

    How to read this: the last branch is the useful one. A file that needs to know about two features is evidence that the boundary is in the wrong place.

    Layout Strength Weakness Fits
    By file type Familiar, nothing to decide A change spreads across folders; hard to delete a feature Small apps, short projects
    By feature Related code together, easy to review and delete Needs a rule for shared code and for cross-feature use Most product apps with several teams
    Layers and slices (FSD) An explicit dependency order, with slices as units More vocabulary and more folders to learn Large apps that need strict rules

    Folder layout also interacts with how teams are organised: one team owning one feature folder maps neatly onto team ownership (see Micro-Frontends and Conway's Law for the organisational side). Barrel files interact with bundling: bulletproof-react recommends importing files directly rather than through barrel files because they can hinder tree shaking, while FSD uses index files as the public API but lists tree-shaking and circular-import problems with them, so this choice has a cost either way (the planned lesson on module boundaries looks at it, and Code Splitting, Lazy Loading and Tree Shaking explains the bundler side).

    The edge case: cross-feature imports and cycles

    Illustration 3 (failure): two features that import each other. The cart shows recommendations from product-list, and product-list shows an "in your cart" badge by importing from cart. Each import looks reasonable alone. Together they form a circular import. FSD's public-API page defines it as "two or more files import each other in a circle", says bundlers struggle with it and that it can cause runtime errors that are hard to debug. Its advice is relative imports inside a slice and absolute imports across slices; the layout above prevents the problem differently, by forbidding the sideways import. A cycle is a symptom that two features share a concept (here, "the cart contents"), and the fix is usually to move that concept down into shared code or up into the app that composes the two.

    In practice: moving a 400-file storefront to feature folders

    NOTE

    This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company or product, and every number in it is an illustrative assumption.

    The situation

    A storefront front end has about 400 files (illustrative) in components/, hooks/, api/ and utils/, worked on by twelve developers (illustrative). A typical change touches five folders, pull requests are hard to review, and two developers deleted "unused" helpers that another page still needed. The team wants feature folders without a freeze on feature work.

    Starting architecture

    Figure 4. Everything imports everything

    Starting architecture: folders by file type, with imports in every directionpages/ to components/; pages/ to hooks/; components/ to hooks/; hooks/ to components/; hooks/ to api/pages/pages/components/: 200 files, all featurescomponents/200 files, allfeatureshooks/: all featureshooks/all featuresapi/: all featuresapi/all features

    How to read this: components/ and hooks/ import each other, and each holds code for every feature, so nobody owns a folder.

    Options considered

    Option What it fixes What it costs
    Do nothing Nothing Review and deletion stay hard
    Move everything in one branch Finished quickly A long freeze, conflicts for twelve developers
    Move one feature at a time, with a lint rule New code follows the layout, old code moves when touched Two layouts coexist for a while

    The decision

    Field Decision record
    Context A by-type layout makes changes spread over folders and gives nobody ownership.
    Decision Create features/, move one feature per week starting with the least shared (search), and forbid feature-to-feature imports and imports from app with a lint rule that starts in warning mode.
    Consequences, good Changes stay in one folder; ownership by feature; code that belongs nowhere is visible.
    Consequences, bad Two layouts coexist; the shared folders need a curator or they become a dumping ground.
    Revisit when The lint rule reports many cross-feature imports (the feature boundaries may be wrong), or the shared folder grows faster than the features.

    Rollout and verification

    1. Write down the rules first: what shared means, the allowed import directions, and the name of each feature.
    2. Add the lint rule in warning mode, count current violations, and ratchet the count down.
    3. Move one feature per week; each move is a mechanical change with no behaviour change, reviewed as such.
    4. Run the build and the tests after each move, and check for new circular imports with a dependency tool.

    The 2 a.m. failure

    After the cart and product-list moves, the production page for one route shows a blank screen with an initialisation error, while development works. The new features/cart/index.ts re-exports a component that imports from features/product-list, which imports back from features/cart. In this scenario the cycle only breaks in the production build, because the order in which modules are evaluated depends on the entry point and on how the bundler lays out the output.

    First three checks: (1) read the error and the first module in its stack; (2) run a dependency check for cycles on the two features; (3) look at the most recent move and its imports. Mitigation: revert that one move. Follow-up: move the shared "cart contents" concept to shared code, make the lint rule an error for cross-feature imports, and add a cycle check to CI.

    Architect's notes

    • Decide shared versus feature on day one, with a one-sentence rule everyone can apply. Shared folders without a rule become dumping grounds.
    • Keep nesting shallow. The React FAQ suggests three or four levels at most.
    • Make a feature small enough that one team owns it and large enough to have a user-facing name.
    • Enforce the dependency direction with a tool, because a convention that only lives in a document erodes.
    • Migrate by moving code when it is touched, not in one big change.

    Common mistakes

    • Making shared the place for anything used twice. Two features using a file is not the same as it being generic; it may belong to one of them.
    • Sideways imports between features, which create cycles and tie two teams' schedules together.
    • Copying a large methodology wholesale before the codebase needs it.
    • Folder names by technology (redux/, hooks/) inside a feature, which recreates the problem one level down.
    • Treating the layout as permanent. Move it when the product changes.

    Key takeaways

    • React has no opinion on folders; by-feature and by-type are both documented approaches.
    • Group by what changes together: a feature change should land in one folder.
    • A layout is a dependency statement; write down which folders may import which.
    • A common one-way direction is shared, then features, then app, with no imports between features.
    • Feature-Sliced Design adds layers, slices and segments; use it when the codebase needs the strictness.
    • A file that needs two features is evidence that a boundary is wrong.
    • Circular imports are a symptom of shared concepts hiding in two features.
    • Enforce with a lint rule and migrate incrementally.

    Practice exercises

    Exercise 1 (Starter, 30 min): List the folders of a project you know. For three recent changes, count the top-level folders each touched. What would the count be by feature?

    Exercise 2 (Intermediate, 2 hours): Draw the import graph between your top-level folders. Mark every cycle and every import that goes against shared, then features, then app.

    Exercise 3 (Advanced, half day): Write a migration plan for one feature of your project: the files to move, the new public API, the lint rule, and how you will know the move changed no behaviour.

    In this course:

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments