Intro
This writing was inspired by the article: “How to Structure Vue Projects”.
It explains various front-end application structures. It’s really insightful and can be applied to other front-end frameworks.
A year ago, my team and I started a new project from scratch. The discussion about its structure and stack became crucial. We decided on a modular monolithic architecture.
I will detail what motivated our choices and hope these insights will inspire you for your future projects.
context
Through my experience, I have worked on several projects. Each with its unique specificities which can be classified into these categories:
Monolithic
A monolithic architecture is an all-in-one project containing all application functionalities.
- Advantages: Easy to onboard new team members, maintain, update dependencies, and add new features. Deployment is straightforward and depends on one CI/CD pipeline.
- Disadvantages: As the engineering team grows, it leads to more conflict management. Deployment can bottleneck when two teams try to release simultaneously. There is no clear ownership of features.
In short, it doesn’t scale well.
Micro-front end
A micro-front end architecture is a spliced application where each feature lives in a separate project, owned by a dedicated team.
- Advantages: Each team can add new features without conflict or deployment bottlenecks. Each micro-front end has its own CI/CD pipeline, allowing specific actions for each project.
- Disadvantages: Harder to onboard new team members and update dependencies. It requires more configuration and maintenance, often necessitating a dedicated DevOps team.
In short, it scales well — until it doesn’t.
What happens when you need to scale down?
In 2023, team reductions due to layoffs lead to shared ownership and less-maintained parts. Usually pipelines and configurations, leading to poor developer experience and slowing down the entire process.
I wouldn't advise any company smaller than FAANG size to go for it.
With that said, let’s introduce the architecture we decided to use.
Challenges:
- Enabling multiple teams to work on the project without conflicts
- Maintaining the project by one or several teams
- Having a single project that is easy to maintain
- Ensuring low complexity for easy onboarding
- Scalable architecture that can scale both up and down
Modular monolith
A modular monolith is an all-in-one project where each feature lives in a separate shell.
In other words, each feature has its own folder and does not share any components, state, or logic with other features. If shared logic, state, or components are needed, they are stored at the root of the project.
This architecture is flexible and can be adapted to your stack and needs.
For example, you can add layouts
if you need layouts for your pages or containers
if you are working with Redux.
Architecture details
I will detail the choices made for our stack and the conventions implemented to address the challenges.
Conventions
- Avoid the trend tech syndrome: New JavaScript libraries are released frequently, making it hard to stay focused on a stack. It might seem like there's always a library that solves an issue in a more elegant way. Don’t fall into this trap. Stick to the stack you have defined.
- Avoid nesting in components folder (The atomic design trap): It's common for people to store components related to a parent component within its folder. Leading to a very nested structure that is hard to navigate for newcomers. Prefer a flat structure within your components folder.
- Put Logic in Hooks: Start by writing the logic within your component. When the logic takes up too much space, move it to a hook. This approach is subjective and may vary for each individual.
- Place testable logic in utils: Separate logic that requires native hooks (useState…) from the logic that doesn't. Move the latter to a utils file if it needs to be tested. More details about this are in the testing section.
- Maximise the utility of third-party dependencies: Avoid adding a dependency that can handle multiple things if you only use one of its features. For example, using RxJS to handle simple HTTP requests when the Fetch API can do it just as well.
Stacks
Shared state (stores
in structure diagram)
Shared state management is always a highly opinionated topic. There are different preferences based on various factors:
- Redux: Preferred by those who like a stricter pattern, a strong community, and solid debugging tools.
- MobX: Preferred by those who prefer more freedom in implementation and a fine-grained updating system to improve optimisation.
- New Trends (Jutai or Zustand): Preferred by those who enjoy new trends because they are hook-like and easy to implement.
Our choice was React Context.
Some may argue that React Context, if not memoized, will re-render all children, and the syntax can be cumbersome.
They would be right.
However, it is native to React, has a low learning curve, and in version 19, the syntax will be simplified.
The Virtual DOM (VDOM) is performant, and re-rendering all children rarely causes issues. If it does, just memoize it properly. React has announced their new React-Compiler (see React-Compiler: When react becomes Svelte), which will optimise during build time.
Styles
CSS-in-JS (styled-components)
Should I really explain why it’s a bad idea ?
- Size of the documentation: Why do we have migration guide when dealing with CSS ?
- JS handling CSS: Why Javascript would be better than CSS at handling CSS ?
- Component instead of class: Why do we need to create a component to apply border styles ?
Don’t recommend to use it in a project
Tailwind
Inline styling done with elegance
- Locality of behaviour: Styles are directly within the component
- Easy syntax: Simple and intuitive
- Optimised: Minification and network compression
I highly recommend Tailwind for any project.
However, newcomers may face a small learning curve to memorise each classes. To onboard people quickly and easily, we decided to go with the following:
Inline-style
The easiest way to handle CSS
- Locality of behaviour: Styles are directly within the component
- Super easy: Straightforward and quick to implement
If we needed selectors or animations, we used:
CSS modules
Namespaced css
- Local to your component: Namespaces classes to your component
- Native css syntax: Uses regular CSS syntax
Tests
Unit test (components)
I strongly believe that unit testing components is a waste of time and energy.
Components will be tested manually with or without unit tests. Testing things that can be easily spotted by eye is meaningless.
The only case where I’ve seen a benefit is when working on a UI library that serves several projects.
Unit test (logic)
As previously mentioned, if you need to test logic, extract it from a custom hook and place it in a utils file. Separate the logic from native hooks (e.g., useState
, useEffect
) and test the logic itself.
Example:
If a custom hook formats data before storing it in a state, extract the format data logic into a utils file and test it there.
Testing a function is easier than testing a hook.
End 2 end testing
I strongly believe that unit testing components is a waste of time and energy because of E2E testing.
Testing a user scenario covers more than just components. It checks if all features of your application are well-implemented together.
E2E testing ensures that critical user paths are working properly: logging in, buying, liking, sharing, etc., depending on your application's business model.
These tests are directly linked to the value of your product. It is more crucial than making sure a FAQ button dispatches an event properly.
conclusion
Each project has its own specificities, and each team has its familiarities with stacks. This architecture and stack won’t fit every project.
The best stack is the one you know
An expert in JavaScript will build a better app in JavaScript than in Rust, even if Rust is known for its performances.
We were a team of five (designer, product manager, engineering manager, and engineers) working on this project. This was aimed to be an internal tool used by our company and it works great.
The workflow is really smooth, and other teams can implement features without difficulties.
The success of choosing a architecture for your project relies on:
- What you want to achieve with it ?
- Who will work on it ?
- Who will maintaining it ?
- Are all member of the team aligned with it ?
I hope you enjoyed this article.
If so, don’t hesitate to keep in contact with me:
Stay in touch on X (Twitter)
If not, please feel free to add your critiques in the comments.
Sources
What is locality of behaviour:
https://htmx.org/essays/locality-of-behaviour/
What is modular monolith:
https://www.milanjovanovic.tech/blog/what-is-a-modular-monolith
How to structure a vue project:
https://dev.to/alexanderop/how-to-structure-vue-projects-20i4
Documentation about react-hooks-testing-library:
https://github.com/testing-library/react-hooks-testing-library
Documentation about styled components:
https://styled-components.com/
Documentation about atomic design:
https://atomicdesign.bradfrost.com/chapter-2/
How tailwind optimise for production:
https://tailwindcss.com/docs/optimizing-for-production
What is css modules:
https://github.com/css-modules/css-modules
Top comments (0)