Translated from: Architectures of modern Front-end applications
In the development process, business requirements can vary and change frequently, making it essential to create a flexible, scalable, and maintainable architecture. It is equally important for everyone involved, such as team members and clients, to have a clear understanding of the project. To avoid excessive documentation, frequent meetings, and continuous improvements, we adopted the architectural approach outlined below. This article will explore the advantages of popular architectures and help you choose the best solution to meet your unique needs.
Many people think that the architectures listed below are merely "folder structures," which is just one way to put it. In fact, if you delve deeper, they have several important aspects:
- Module collaboration: Efficient communication and interaction between different modules/components in the application (using component-based architecture, reusable code).
- Improved project navigation: Building projects in an easy-to-navigate and maintainable way (clear folder hierarchy, naming conventions, separation of concerns).
- Separation of business logic and user interface components: Separating business logic (data fetching, state management) from user interface components to enhance maintainability and reusability (e.g., using services or stores like Redux, Context API, etc.).
- DRY: Don’t repeat yourself
- DAC: Divide and conquer
Please see the diagram below—our focus is on the area in the lower right corner, where these principles are most effectively combined:
Ideal result (example)
We aim to achieve these aspects through architecture. Now, let’s analyze each architecture in detail and choose the most suitable one based on specific circumstances.
Classic architecture (without architecture)#
Classic architecture is a method that many people are already using. We typically focus on basic concepts, dividing the project into "pages," "components," "helpers," etc. However, the problem is that as the application grows, the structure begins to break down, making it increasingly difficult to find the right components or their business logic. Let’s illustrate this with an example:
Components overuse (example)
In this example, we have 3 pages that are clearly not overused. However, they may contain all the components shown below. If we take a look at these components, we will find real "chaos"🤯: each component actively uses other components, creating dependencies between them. This makes them difficult to extend and reuse.
Here is another example using the Redux state manager:
Scattered application logic - Redux state manager (example)
In our setup, each component is managed by a designated "reducer," which is responsible for handling its specific logic. However, some component logic has been incorrectly placed in the wrong reducer. This may happen because developers do not notice existing files or do not consider creating a new file due to the limited amount of logic needed at the time. As a result, some component logic is now scattered throughout the project, making it less clear and harder to maintain.
The following diagram illustrates a missing architecture:
Destructive decoupling diagram (example)
This approach (or more accurately, the lack of architecture) often leads to a chaotic environment where dependencies are difficult to track, resulting in confusion and making it hard to support the project. However, this approach may still be useful in some specific cases, such as:
- Small teams (1-2 developers)
- MVP projects
- Projects not intended for long-term support
- Learning projects or templates
Modular architecture#
Modular architecture is a method of dividing the application into several layers (pages, modules, components, UI, etc.), where independent modules already exist within these layers, each with its own logic and scope of responsibility.
Modular architecture -- layer structure (example)
In this example, the arrangement direction of the application layers is consistent: pages → modules → components → UI (the opposite if viewed from the other side). This means that the higher the layer (e.g., pages), the fewer lower layers can be used—components cannot use modules but can use everything in the UI layer, while modules can use components but cannot use pages. Pages can only use modules.
Modular architecture -- modules and public API (example)
As mentioned earlier, each module has its own scope of responsibility. It is also important that each module should have its own public API (index.ts file), which encapsulates all internal logic of the module and only exposes what is needed externally. (This is very similar to OOP principles: when a class has many private methods, those methods cannot be accessed from outside but can be used internally within the class.) Speaking of pages files, it is quite simple: ideally, it should just be a wrapper for modules and components, with all business logic of the application placed at the module and component levels.
(⚠️) Important: One module should not use another module, and one component should not contain complex logic. If logic is still needed, it should be as simple and maintainable as possible; otherwise, it becomes a module!
Please see the diagram below:
Almost Ideal diagram (example)
However, we still have global directories like components/ and ui/ that may be overused. In some cases, logic may grow, making it no longer clear what constitutes a component and what constitutes a module. Additionally, we often find that as the application grows, developers begin to use modules in other modules, which undermines the principles of that architecture and creates unnecessary dependencies. Nevertheless, our architecture provides the following features:
- Single-threaded
- Ability to reuse components at different levels
- Nearly perfect sense of hierarchy
- Encapsulation
Feature Sliced Design architecture#
Overview - https://feature-sliced.design/
Feature Sliced Design (FSD) architecture—similar to modular architecture but avoids the situations we discussed above. This approach builds projects by functional areas (features) rather than by layers. This organization helps avoid the growth of global directories (like components, UI in modular architecture) and provides clear divisions of responsibilities between components, modules, and layers.
FSD - layers example
The way the architecture is built is that the top-level pages integrate and organize the work of all sub-modules and components, while each layer provides more detailed and specific functionalities and elements. Yes, we follow the same rules here—the higher the level (e.g., pages), the fewer lower levels can be used:
- Pages - The top level contains the pages displayed in the application.
- Processes - This layer has been deprecated in this architecture, so we can skip it.
- Features/Widgets - Below pages are the main feature blocks that build the core functionality of the pages, making them manageable and independent.
- Entities - The entities below the feature modules are compiled from simpler UI components available in the "shared" layer.
- Shared - The bottom layer contains common UI components that can be used across different parts of the application.
(⚠️) Important: As with modular architecture, do not overuse between layers.
Overview (slices/segments) - https://feature-sliced.design/
FSD architecture features modular elements called "slices" and "segments." "Slices" refer to the modules within each layer, with each module representing a different business entity. Meanwhile, "segments" contain various structural components, such as api/
, components/
, config/
, constants/
, etc., organizing the architecture into clearer and more manageable parts.
Finally, we achieve the target result:
Ideal - diagram (example)
Integrating this approach into a project is neither easy nor quick. It requires at least a minimal understanding of architecture, and you must keep in mind that this may take time. However, mastering how to use FSD can lead to:
- Clear structure
- Distinct hierarchy
- Flexible components
- Independent modules
- Balanced reusability
By the way, this architecture advocates using "kebab case" in file naming👀.
product-description.vue
/shopping-cart.tsx
/get-base-url.ts
/ etc.
Examples of using "modular architecture":
- Modular in classic React applications
- Modular in Vue3 applications
- Modular in React Native applications
Examples of using "FSD architecture":
- FSD in classic React applications
- FSD in Vue3 applications
- FSD in Next.js applications
- FSD in React Native applications
Conclusion#
In this article, we explored the differences between classic architecture, modular architecture, and FSD architecture, and discussed their uses.