Scaling Frontend Applications
Malte Ubl, the CTO of Vercel, gave a talk at React Summit 2021 on the principles of scaling frontend applications. He narrowed down the principles to six key points that can be applied to any frontend project to create a scalable, maintainable, and adaptable repository.
This article will summarize the key points from his talk and provide practical tips on how to apply these principles in your frontend projects in a practical way.
Watch the full talk: Malte Ubl - Principles for Scaling Frontend Application Development, React Summit 2023 (opens in a new tab)
Teardown barriers
Foster collaboration and ownership through a monorepo structure.
Use access restrictions to specific parts within the monorepo to manage contributions.
Monorepo structure
Monorepos are difficult to setup, however there are powerful tools such as nx
that abstract these complexitites. Check out the Boostrapping a Project section for a detailed guide on setting up your monorepo.
Access restriction
You can restrict certain parts of codebases to teams or users, or require peer reviews from specific people by using the CODEOWNERS
file. This is easy to setup on both GitHub and GitLab.
Make it easy to delete code
Keep the codebase lean by minimizing unnecessary code.
Move data calls and filtering inside components (Data-in-JS) to reduce code clutter.
Domain-Driven Design (DDD)
Domain-Driven Design (DDD) focuses on building software systems around the core domain, representing the application's business logic. Applying DDD principles in frontend development leads to modular, maintainable, and scalable codebases.
By adopting DDD principles in frontend development, you can create a more structured and maintainable codebase that aligns with the business domains of your application, leading to increased productivity and scalability.
Organize code using a directory structure that reflects different domains and subdomains. This makes it so that if we needed to delete anything to do with cart, we only need to remove it in one place:
├── src
│ ├── common
│ │ ├── components
│ │ │ ├── Button.tsx
│ │ │ └── ...
│ │ ├── hooks
│ │ ├── utils
│ │ └── ...
│ ├── domains
│ │ ├── shopping
│ │ │ ├── cart
│ │ │ │ ├── components
│ │ │ │ │ ├── Cart.tsx
│ │ │ │ │ ├── CheckoutButton.tsx
│ │ │ │ │ └── ...
│ │ │ │ ├── hooks
│ │ │ │ ├── utils
│ │ │ │ └── ...
│ │ │ ├── products
│ │ │ ├── orders
│ │ │ └── ...
│ │ └── ...
│ ├── layouts
│ │ ├── MainLayout.tsx
│ │ └── ...
│ ├── styles
│ ├── App.tsx
│ ├── index.tsx
│ └── ...
Read Martin Fowler's DomainDrivenDesign (opens in a new tab) for more insights.
DRY vs. WET
We learn early on that DRY (Don't Repeat Yourself) is the main principle to follow in programming. However WET (Write Everything Twice) is often overlooked and there is a place for a small duplication over the wrong abstraction (opens in a new tab).
When introducing shared code in a project, it can be very easy to just stick anything that "could" be shared into a shared directory. You'll soon find however that if everyone on a team does this, your shared codebase will end up doing everything and anything under the sun, and you'll end up importing a library you wanted to do one small function, but you've also got a library that does multiple things with thousands of dependencies. It's the old You wanted a banana but you got a gorilla holding the banana (opens in a new tab) problem.
Soon enough, multiple teams are deependant on a shared lib that does much more than it should and these dependencies become extremely difficult to rip apart. So when using a shared lib
folder, make sure that:
- Packages are small and have a point (avoid
utils
where possible). - Ask yourself if having this shared dependency is worth the potential future cost.
Data fetching in TS
Delegate data fetching to child components. Not all data needs to be at the top level of the applications. The following code is encouraged as deleting Cart
usages is in one place.
import type { FC } from 'react';
import { Cart } from '@/domains/cart';
type SidebarProps = { cartId: string };
export const Sidebar: FC<CartProps> = ({ cartId }) => (
// Data fetching done in Cart component
<Cart cartId={cartId} />
);
Tailwind
Use a utility-first approach such as tailwind (opens in a new tab) which reduces the amount of CSS you need to write, but it also avoids needing an adjacent CSS file for each React component.
Migrate incrementally
Scale the application through incremental migrations rather than massive overhauls.
Take small steps and gradually migrate parts of the codebase to mitigate risks.
Always get better, never get worse
Prioritize addressing technical debt and improving the codebase over time.
Continuously raise the code quality bar by resolving eslint errors and adding new rules.
Linting rules
Leverage linters to get your codebase to a standard you would like. In line with the Migrate Incrementally principle, make sure to not let violations stop entire codebases from being comitted.
Embrace a lack of knowledge
Document the codebase knowledge in a machine-readable format for easy reference.
Enable team members to quickly understand and collaborate on the codebase.
Documentation
Document how to interact with different codebases and teams. Read more on how in the Documentation and Communication section.
Testing and automation
Integrating testing frameworks and tools for automated testing. Read more on how in our Testing section.
Implement a continuous integration and delivery (CI/CD) pipeline for seamless scalability. Read more on how in our CI/CD section.
Codeowners
As stated in Access Restrictions, add CODEOWNERS
.
Eliminate systematic complexity
Maintain a catalog of difficulties encountered by the team within the app.
Develop strategies, tools, or processes to reduce systematic complexity.
Use tools like zod to keep TypeScript types consistent with APIs for a smoother development experience.
Documentation
Document how to interact with different codebases and teams. Read more on how in the Documentation and Communication section.
Leverage type-safe API methods
Leverage zod
(opens in a new tab) to validate contracts, or GraphQL (opens in a new tab) or tRPC (opens in a new tab) to keep APIs typed.
Keep up to date with any latest changes or announcements by subscribing to the newsletter below.