Organizing Your Frontend Code
The Beginning:
Hi there,
Back in 2021, when the Xflow frontend team was formed, we were fully focused on building the apps from the ground up. Today, we have hundreds of pages that the frontend team owns and thousands of files consisting of components, hooks, utils, and more across five different frontend apps π€―.
We started noticing that many frontend widgets and components became bloated over time! A simple component originally created for purpose 'A' began doing five more things as the product grew over time...
Finding out if some logic already exists in the codebase became as tough as finding a needle in a haystack.
We needed to organize the codebase better, so we searched online for ways to structure a large project to make it scalable and maintainable. However, most online articles, tweets, or talks focus on which UI framework is best, which styling strategy is superior, or which API client is revolutionary. There are almost no articles or talks about how to structure projects when multiple developers are working on a large codebase.
This article aims to share a solution that can be adapted and applied to your teams for scaling projects.
So What Actually Happened?
Given the scale of our frontend applications, the inevitable happened:
We noticed that components and hooks were becoming overloaded and handling too many tasks over time.
The business logic and the component rendering logic was mostly mixed inside all components.
The readability of the code suffered as the size of a component or hook increased.
Developers began defining components within pages. In the future, if the same component needed to be used on another page, it was modified in its original location and then imported into the new page.
Different developers started creating different react hooks calling the same API.
The main issue wasn't just the creation of different hook files, but how the Apollo refetch API works. We use Apollo Client for API calls, and after each mutation (similar to a POST call), we re-fetch some existing active queries to get new data from the server. The problem is that to refetch an active query, we need to provide the exact same request payload as when the query was first fetched. Since there was no centralized place for writing the request payload for the refetch call, sometimes the refetch didn't execute when payload given was incorrect, resulting in stale data being displayed on the frontend. - For example, when we create a new invoice in the dashboard using button A, developed by developer A, we might not see the new entry in the invoice listing page. However, the same action would show the new entry when using button B, made by developer B, due to the correct usage of the Apollo refetch API.
We began to notice code duplication over time because it was hard to determine if the logic already existed.
Code reviews became difficult over time when multiple files were changed as part of a pull request.
Most importantly, it was difficult to find any file in the codebase without banging your head! π€
Oh! Was there ZERO app structuring before?
No, not true. We had decent app structuring from day 0 itself.
In most cases, an application communicates with multiple backend services. Therefore, we have a BFF (Backend for Frontend) server that interacts with these various backend services. All frontend apps communicate exclusively with this BFF for any API interactions.
This BFF is a NestJS-based service that helps us create independent modules. Using NestJS modules, we made separate modules for each backend service, which greatly improved our code structure.
Moreover, we realized early on that different frontend apps should be independent, with only common components and setup files being shared among them. So, we created a monorepo to maintain separation of concerns while keeping all the apps under one roof. However, if we look inside the folder structure of an individual frontend app, there were no guidelines at all.
Okay, what was done finally?
Part 1: The Clean architecture
As a result, we created three main folders for organizing our code, each with a specific purpose:
Core - This layer contains the component rendering logic. This layer can be considered as a dumb layer whose purpose is to just render components.
Domain - This layer includes the product-related conditions and validations needed to render the components.
External - This layer handles all the external interactions from frontend application like API calling, etc.
βThis solution is loosely inspired by βThe Clean Architectureβ by Uncle Bob.β
From the diagram above, nothing in an inner circle should know anything about an outer circle. Specifically, the name of something in an outer circle must not be mentioned in the code of an inner circle:
The core layer can access the domain and external layers.
The domain layer can only access the external layer (it cannot access the core layer).
The external layer cannot access anything!
Part 2: Inspiration from BFF (Backend for Frontend)
For each root folder (core, domain, and external), we added sub-folders for each backend entity, similar to BFF!
For example, let's take a look at the External folder:
We have a "payments" module in both BFF and the dashboard frontend app.
The APIs declared in BFF (listPayments, retrievePayments) map directly to a React hook in the dashboard frontend app.
Part 3: Final solution:
Let's put everything together and look at the new project structure through an example below:
Assume there is a "WithdrawWidget" component that shows the USD balance for a user and allows them to withdraw that balance. Originally, this component was overloaded with all the business logic, validations, and more, all in one file. We applied the new framework to it. Check the diagram below:
The modules flow all the way from backend to frontend apps (balances, accounts, etc.).
The "WithdrawWidget," which shows the USD balance for a user and allows withdrawing that balance, is placed inside the "core/balance/" folder.
- This widget can access both the domain layer and the external layer.
Business logic, such as withdraw only if balance is non-zero and withdraw only when currency is USD, is placed inside the "domain/balance/" folder.
- The Balance domain can also use the Account domain, for example, withdraw balance only when account status is not equal to HOLD.
The API fetching logic is placed inside the "external/balance/" folder.
- Bonus: We now have a single place to refetch any query after an API call (mutation), which solves the Apollo client issue for us.
Yay! What advantages did we gain?
Tech advantages:
Multiple developers writing duplicate logic and creating the same API clients inside their components is now eliminated.
Testable. The business rules (domain layer) can be tested without the UI, database, web server, or any other external element.
We can easily replace external libraries like "apollo client" from the external layer with another library.
The Apollo refetch query issue is resolved.
Apart from all the technical benefits discussed in this article, let's understand a few advantages from a code management perspective:
We now have module owners within the frontend team. Each developer owns a few modules end-to-end, including making sure proper tests are in place and ensuring that code is properly reviewed for their respective modules as part of any Pull Request.
For a developer who joined recently or a developer debugging a production issue, the code is very easy to read and navigate. Product code can be easily changed without getting lost inside the core UI rendering code.
The Conclusion:
We encountered this problem as we scaled, and it was nearly impossible to anticipate it from day one. We learned, adapted, and tried to come up with a general solution that can be applied to any team.
Thanks for reading !!
Alright! You've made it to the end of this article. If you enjoyed it, please give it a like and drop a comment about your favorite part.
Subscribe to my newsletter
Read articles from Sagarpreet Chadha directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by