An entry that will try to dispel any doubts about the use of modular architecture in frontend applications.
There is nothing more beautiful in a developer's career than well-designed architecture.
Good architecture is easy to recognize. Ask yourself simple questions. Is what I'm doing obvious? Is it repeatable? Doesn't my work block others?
If all the answers are true, then you are working with a well-designed architecture.
What it means in practice? Together we will find the answer.
1. The problem
Let's say you have a simple feature to implement. It will be loading users and displaying loader/error based on API response.
Your code may look like the code you are about to see. If that's the case, you'll have problems that show up late, or to be more precise, as new functionality is added.
The issue will grow with the amount of functionality and the number of developers in the project.
Such an approach is vulnerable to changes in requirements and it violates the open/closed and single responsibility principle from SOLID.
Your current architecture diagram
Let's refactor this code and design real architecture step by step.
If you write an entire application like this, you have to reckon with the fact that it will not be stable and friendly to changes in business requirements.
Each application should have layers with clearly defined relations. When I say layers, I mean code fragments that are logically related to each other and recur across different functionalities.
An example of such a layer could be logic for API requests or business logic. In addition to identifying it, you still need to name it somehow, create abstractions and design relations.
In frontend applications, we can easily separate two layers - logic and presentation.
Now you have 2 layers
To be honest - it's not enough. The presentation can be split into additional layers and the same for logic. You should achieve this because if not - you are not able to reuse code.
3. Design phase
Firstly, check this diagram.
Modular architecture diagram
These blue boxes represent features. Each functionality should take advantage of this structure and relations. These rules are crucial when the solution is growing 🐍.
This approach gives us a division of application code and generic code that can be used in other solutions. Also, you can easily publish libraries via npm or migrate to monorepo later.
First, let's create some components and place them in the ui directory. This library will store purely presentation components, you can treat it as a design system implementation.
Next, we will create a development-kit library that will store generic logic to handle API requests, rendering and anything that will be useful in various applications.
Now when you create another app, you already have some base - design system and a set of useful features that speed up the work.
If you are interested about useFetch hook you can check useFetch hook article.
Let's start with the service layer that will be responsible for encapsulating the logic, performing API requests and carrying out the necessary data mapping.
Now, look at the next layer - logic. We will use useFetch generic hook to implement users fetch.
Logic done. Now it's time for the next layer - components, which can be used only inside this application. This layer can use models from the application domain.
It's time to glue everything with container. The layer that is supposed to bind the logic to the component.
Last layer will be a module. A starting point for using the functionality. Here you can add things like lazy loading or authorization logic.
For easier recognition of individual layers, you can add a post fix to the file name - Angular is doing that and me too.
6. A few doubts
What about the situation when module A should consume something available in module B? It will probably happen when you would like to implement an authorization mechanism in the app or something similar.
You can use the same approach as in libs directory. Just create shared folder and put there all reusable stuff connected with application.
Architecture with shared module
7. Adding authorization
Let's imagine a situation when UsersModule is a protected view. Only authorized users can access it. Check the gif below to understand how it should work.
To access authorization state in different modules and be able to trigger authorization we need Context API from React and AuthModule itself. This stuff will be added to shared directory.
As before we will split it into layers to achieve goals. Firstly, we add service.
Next, we need authorization logic.
It's time for module. It groups the whole feature.
Now we need to wrap our entire application with AuthModule. Now you can use authorization!
Last step - just consume authorization API via useAuthContext hook and decide what should be displayed.
8. When to use?
Consider using this architecture when at least 2 or 3 of these points are truthy.
- It's a single page application
- There is a small amount of shared state across components
- Application is small/medium size
- Your team is familiar with these architectural concepts
I hope you enjoyed my architectural proposal. It's mostly based on Angular's one.
It's not a perfect solution - as always it depends on the project. However, you have the next skill in your spellbook and you should be able to use this kind of approach.
Feel free to contact me if you have any questions/proposals. Have a nice day and good health!