A pragmatic guide to structuring complex frontend codebases
It’s no news that the world of frontend technologies is ever evolving and at a swift pace. As browsers get insanely capable — be it at reducing their share of CPU utilization, prioritizing the active tab or at improving power consumption, the modern Product Manager sees no limits in how much a single-page app can achieve on the web and rightly so.
Having said that, what does this mean to be a modern-day frontend engineer supporting a business team that knows no limits in defining new feature sets and expecting rapid time-to-market. Well, they need to be exceptional coders, knowing every latest trick-of-the-trade and that’s just a start.
While it’s a top priority of every engineer to develop and roll out valuable features with good quality, what makes a good frontend engineer great, is his constant endeavour for code-sustainability in each & every commit. The need of the hour in writing sustainable code is managing the growing size of today’s UI codebases.
At Sixt, as digitization is picking up rapid pace and teams are building massively elaborate functionalities, I am sharing a true account of learnings from scaling a React codebase from what was a basic create-react-app scaffold into quite a massive mono-repo.
- Start small — every large codebase is once a scaffold
In the early phase everything is green, and developers love new ideas. But what was critical to our success was that we realized the scale of the application and had a strong vision in terms of where we saw the application codebase heading.
A single-page react app typically has several moving parts; just to name a few:
I. The root/component tree component
II. bundling configurations
III. CSS preprocessor
IV. Unit testing library
V. React error boundary & centralized error handling & logging
VI. Routing for navigation
At the time of development of first few application features, the last thing you need, is to realize a key technical component is missing and that it’s a blocker for progress. We benefitted quite a lot from identifying these core components of our architecture and wiring them upfront, so that all the heavy-lifting is done before picking up any feature development.
However, in this phase we better be careful not to fall for the over-engineering trap and keep the codebase lean.
2. The file structure debate — rails style, redux ducks & more
We realized from past experience that well thought-through file structures & conventions dramatically improve developer productivity. After much deliberation, the structure that achieved consensus was a domain-driven one and brought in a sense of ease when we started working with the code through our initial sprints. Below is a snapshot of a sample file structure.
The pseudo folder here, called “main-feature” encompasses the container directory which hosts stateful components, along with feature-specific adapter/mapper and an “api” component that connects to the backend REST APIs.
The “core” folder hosts stateless atomic components. The idea here is to enable seamless compatibility and reuse within other features or even among separate React web projects.
3. Create the router component early zeroing-in on all the navigation requirements
The web application we were building would have to deal with several nested routes. This had to be clarified early on, so we adopt the right browser router to suit our needs. It turned out that the Switch component with React Routes would be the right choice as we wanted to pick the exact route comparing against several sub-routes.
<Switch>
<Route path=”/vehicles/new” component={AddForm} />
<Route path={`/vehicles/:vehicleId`} component={Profile} />
</Switch>
Say we use the above code in a <Router/> component — we would see that both {AddForm} and {Profile} would render, since “/vehicles/new” could look like either Route to a Router component.
4. The hexagonal architecture & domain driven design on the front end
While several established codebases on the backend have adopted the ports & adapters design pattern to much success, we did benefit a lot by embracing the same principles of code structuring on the frontend.
Layers know each other from the outside-in:
Adapter: Hosts code related to technical implementations of infrastructure and not the business abstractions
Application: It represents the use cases with the domain naming. It’s like a transactional barrier on events triggering and business logic.
Domain: It represents the models of our business (value objects & entities). The domain services are included in this layer too.
How it works
The Domain layer is known by the Application layer, and the Application layer is known by the Adapter layer, this way, if one needs to make changes on the infrastructure/underlying technology (new framework, ORM…) it won’t affect the rest as there’s minimum coupling.
For example, the application layer would communicate through an interface of the repository on the Domain layer. The implementation of such interfaces will be on the adapter/infrastructure layer following the adapter pattern.
Hexagonal architecture applied to frontend projects
As stated in the file structure section, the domain-driven approach to structuring the frontend codebase ensured that we have a loosely coupled & a highly cohesive structure. This brought in a long tail of benefits — ranging from better testability to minimising code conflicts and ensuring flexibility while we picked up application/business user stories.
5. Evolution of the codebase into a mono-repo — let it happen organically
While we never set out to build the project as a mono-repo, we eventually landed up with a use case that did justify a lot of benefits from moving the already large and fast-growing codebase into a mono-repo.
For starters, what exactly is a mono-repo?
“Monorepo is a combination of two syllabic abbreviations, i.e. mono and repo, where ‘mono’ refers to monolithic and ‘repo’ means repository.”
In unison, we’re talking about the monolithic repository which can further be defined as- A software development strategy to store all the isolated code parts from multiple projects, in a single repository.
Mono-repos uphold the three cornerstones of coding:
1. Keeping the code simple
2. Reducing code repetition- DRY Approach (Don’t Repeat Yourself) -
3. Rule of composition — Separating out simple objects and putting them together to form complex ones.
One use case that led us into refactoring the code into a monorepo was when we realized, there was scope to extend the project to also support another new dependent web project that required pretty much most of the “core” components and would also benefit a lot from the plumbing code that held the existing application together — things like Webpack bundling, reusable React components, HTTP/Ajax modules — none of these needed to be re-ported or redone. In other words, “no need to reinvent the wheel”.
With a strong unit testing foundation using Jest & Enzyme, coupled with some of the above structural design patterns, we knew our code is in good stead for many a challenge and that it would stand the test of time.
This confidence is among the biggest outcomes of starting small, staying nimble while staying true to one’s own vision in seeing a large front-end project through.
Hope you had more than just a takeaway from this real-world experience. Thanks for reading!