Frontend projects are like snowflakes - every project is unique. There is no popular, widespread way of structuring a frontend application. Every React project has its own guidelines. And while some may say that Angular projects are more unified, I question if this is actually true, given that Angular itself provides many ways to work with modules (feature based, SCAM, standalone, ...).
The truth is: the only right way for structuring projects doesn't exist. It depends on your requirements and the tools and frameworks you use. In this article we'll discuss three different project structures.
Structure by type
Structuring files by their type is common in tutorials and small projects. You create separate folders for components, hooks, routes and more.
src/
├── components/
├── hooks/
├── routes/
├── types/
└── utils/
The pattern is simple and easy to apply. But the truth is: it doesn't scale. The structure works fine for very small projects or simple websites. For any real application I don't recommend it.
If you want to learn more, read the article Delightful React File/Directory Structure by Joshua Comeau.
Let's move on to another, better way to structure projects.
Bulletproof React
Bulletproof React is a set of guidelines for React applications. Not just about the project structure, but about styling, linting, testing and more. I want to focus here on the project structure.
The recommended project structure is based on feature modules with some global, shared components and hooks:
src/
├── assets/
├── components/ # Shared Components
├── config/
├── features/ # Feature modules
│ ├── my-feature/
│ │ ├── api/
│ │ ├── assets/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── routes/
│ │ ├── stores/
│ │ ├── types/
│ │ ├── utils/
│ │ └── index.ts # Public API
│ └── another-feature/
├── hooks/ # Shared hooks
├── lib/
├── providers/
└── routes/
Most of the code lives inside the features
folder, split into separate features. The files inside a feature folder are split by type, like components and hooks. Each feature module has an index.ts
file that acts as a public API for other feature modules. Other feature modules are only allowed to use stuff that is exported from the index.ts
file.
Components and hooks that are shared across all feature modules live in src/components/
and src/hooks/
.
Overall, it's pretty good structure and works for a lot of projects. Working with feature modules helps you to keep your code properly structured into small, easy to understand pieces.
I see a few limitations when using this approach:
- Structure by type: Structuring files by type (components, hooks, ...) on a lower level (like inside feature modules) works fine most of the time. But sometimes you have components and hooks that are tightly coupled together. You then either have to split them across two folders or violate the guidelines. Neither is a good solution.
- Dependencies between features: Feature modules have dependencies between each other, that's inevitable. Unfortunately, they are not transparent and don't know on what a feature depends on. If your feature modules are not properly split you may end up having circular dependencies.
- Global/feature dependencies: Having a look at the example app implemented by Bulletproof React, you will see that global files access feature modules and feature modules access global files. There are no guidelines for these dependencies, and it may get messy if you don't handle this carefully.
- Community: There is no real community around these guidelines. There are documented and you can use them. But there are no discussions and rarely any improvements on the docs.
Let's move on to the last approach, Feature-sliced design.
Feature-sliced design
Another, probably less known way for structuring projects is Feature-sliced design. It's based on layers, slices and segments.
Structure
Let's take a closer look at the structure:
Layers
The top level directories are layers, split by their responsibility. The layers are:
app/
: Application initialization logic and static assetsprocesses/
: Workflows involving multiple pagespages/
: Complete application viewswidgets/
: Various combinations of abstract and / or business units from lower layersfeatures/
: User scenarios, which usually operate on business entitiesentities/
: Business units in terms of which application business logic worksshared/
: Reusable non-business specific modules
You may not need all of these layers. For example, if you don't have any workflows involving multiple pages you can omit the processes
layer.
Dependencies between layers are strictly limited: Higher layers can depend on lower layers but not vice-versa. In example, the app
layer can depend on any other layer, shared
cannot depend on other layers and features
can depend on entities
and shared
but not on widgets
, pages
, processes
and app
.
These rules not just help to avoid circular dependencies between layers but also make it clear on what a layer may depend on. In addition, it helps to understand the impact of changes inside a layer. Changes in the shared
layer may affect many other layers (and are therefore more critical), while changes in the processes
layer only affect the app
layer.
Slices
Inside the layers you create slices
, which partition the code by business domain. Slices are not regulated by the methodology but depend on your project. You may have an entities/post/
slice that represents blog posts or a features/write-comment
slice to write comments.
Segments
Each slice is split into one or more of the following segments:
ui/
: User Interface components and UI related logicmodel/
: Business logic (store, actions, effects, reducers, etc.)lib/
: Infrastructure logic (utils/helpers)config/
: Local configuration (constants, enums, meta information)api/
: Logic of API requests (api instances, requests, etc.)
Comparing
Compared to Bulletproof React, Feature-sliced Design is a bit more complex to understand and apply. But in my opinion it's worthwhile to adapt this structure for a few reasons:
- Layer dependencies: Layers can depend on each other but only in one direction. This makes dependencies easier to manage and prevents you from running into circular dependency issues.
- Reusability: The layers and their dependencies encourage reusability. Features on lower layers may be reused on multiple higher layers, like different widgets or pages. In other methodologies, to make code reusable you either have to move code into global folders or extract them into feature modules that aren't features but just exist for reusability purposes only.
- Composition: The methodology encourages component composition. Higher levels like widgets compose their UI based on elements from lower layers like features and entities.
- No separation by type: You don't split your code by type, i.e. you don't have components and hooks folders. This allows you to colocate code that belongs together.
- High cohesion: You may argue that the code for a page is cluttered around the whole code base because of the layers. It's true that the code for a page is distributed across the layers, but that's for good reason. Not just for reusability and composition, as mentioned above, but it results in slices with high cohesion.
- Community: Feature-sliced design has an active community that will help you to adopt the methodology. It's mainly a Russian community, but you can ask questions in English too.
Summary
We discussed three different ways for structuring your frontend projects. Structuring by type works well for small, simple projects. Bullet-proof React provides a good project structure, but you can scale much better using Feature-sliced design. Give it try!