3D blocks with htmls, css and js in front of a screen on a violet background

Photo Credit: Philip Oroni

Frontend Architecture Cheat Sheet

October 29, 2024 Frontend architecture

Frontend architecture is much more than folder structure, or deciding React vs Angular vs whatever is the hype nowadays. It entails combining expertise from different fields and think holistically about the application, taking into account the context in which it is being built. Software architecture in general is a multi-domain discipline, and it's easy to get lost in the details and loose the big picture. I wanted to assemble a cheat sheet that would be a sort of mind map for thinking about frontend architecture. It combines rules, processes and distilled knowledge about designing scalable frontends. It was a challenge since I wanted to have a short but exhaustive list of things to check and remember about without getting into too much details.

Software development rules

Before diving into the cheat sheet, it's important to remember about ground rules. They are common to any software architecture and I use them to set my mindset for evaluating anything related to software development.

Rule 1: Keep it simple

Don't overthink your solutions. It should be easy to understand and apply.

Rule 2: The best architecture is flexible and allows easy changes

There are a lot of best practices that help achieve this. My favorites are:

  • Single responsibility principle (SRP) - applied across all level of abstractions, from services through modules to the smallest components.
  • Separation of concerns, low coupling and high cohesion - together they help reinforce SRP, and organize the code into logical pieces that just fit together.
  • Core actions and business logic should be separated from UI frameworks.

Rule 3: Start small and evolve along with the product

At the beginning of a new project it's hard to predict which direction it will go.

There is nothing worse than the wrong abstraction. Some code repetition is OK, and flexibility is much more important than coding purity.

In time, more patterns will emerge and the architecture will be reflecting them naturally. The correct and useful abstraction will become evident in the process

Rule 4: It's all about tradeoffs - You need to choose the problems that you want to have

There are no silver bullets. Every decision is a trade-off, and will bring its own problems.

As conscious developers, we have control over which problems we want to have.

Rule 5: Architecture is a product of its context

It's not only about pure technical things. Architecture will be affected by many different factors and constraints, ex.g.:

  • Client budget
  • Deadlines
  • The product's audience
  • Incoming requirements
  • The team's expertise. Can your team adapt quickly to a technology that you are considering?
  • The current trends in software development.
  • Etc...

Architecture characteristics

When choosing an architecture, it's good to identify common characteristics that can be rated on a scale (ex. 1-5).

This resulting matrix can be helpful for making the final decision. In terms of Frontend architecture, I borrowed and adapted a list compiled by Luca Mezzalira in his fantastic book Building Microfrontends:

  • Deployability - Reliability and ease of deploying
  • Modularity - Ease of adding or removing components
  • Simplicity - Ease of being able to understand and work with an architecture
  • Testability - Degree to which a software artifact supports testing in a given context
  • Performance - How well the app would meet the quality of user experience.
  • Developer experience - the experience developers are exposed to when they use your product, be it client libraries, SDKs, frameworks, open source code, tools, API, technology, or services
  • Scalability - The ability of a process, network, software, or organization to grow and manage increased demand.
  • Maintainability - Ease of evolving the architecture and keeping its components in sync and up-to-date. This involves dependency management strategies and avoiding vendor lock-in.

The architecture design process

To compile this list, I took a lot of inspiration from the System Design discipline, and the RADIO framework. Frontend applications have a unique context and set of challenges that require UI-specific architecture and software patterns.

📝 Gather requirements

Every project or task begins with a good understanding of requirements.

  • Architecture should adapt to the product that you want to build, not the other way around.
  • The reality of product development is that there are always constraints - budget, time, team, product audience and future requirements. Understanding them is a part of the job.

💻 Architecture top-level overview

This stage results in creating the base structure of the application. It should include decisions about domain boundaries, core frameworks, the flow of data and how the application will be rendered.

  • Identify the core domains (Domain Driven Development is a good inspiration).
  • You need to choose the domains that should be developed internally and ones that should be adopted from 3rd parties and vendors.
  • Domains have modules, modules have components. Each level is a different abstraction, and business logic should be kept higher up in the chain, to make the modules and components easier to reuse, swap or change.
  • Some modules/components can be shared so it's important to think well about the sharing mechanisms and how to split responsibilities.
  • When deciding the folder structure, start simple, and don't make too many subfolders before it becomes necessary. In the frontend world, co-locating files of different types but belonging to the same module (ex. posts , user ) is easier to manage than splitting files into different folders by file type (ex. views , models, controllers ) - which is a pattern still often used in the backend world, especially by MV-whatever frameworks.
  • The UI changes very fast. In order to support quick iterations, your architectural design needs to be ready for a lot of moving around of components, state and other pieces.

Choose your base architecture, like 3-tier, MVC, SPA, Micro-frontends. Several factors will help you decide:

  • What will be the application rendering model? SSR (Server-side rendering), CSR (Client-side rendering) or hybrid?
  • What are the SEO requirements?
  • How will components communicate with the database and core business layer (a.k.a the backend)?
  • How many developers or teams will need to collaborate on the project?
  • What is the deployment speed that you want to have?
  • What is the backend and infrastructure architecture? Will they be developed in-house, or will you be communicating with a serverless 3rd party provider like Firebase or AWS?
⚠️ Never choose a complicated architecture, like Micro-frontends at the start of the project. It's always best to start with a simple, established pattern that is easy to refactor. Once a project will become mature and the domains will be well-defined, you may have a real business case to justify a more intricate architectural pattern.

Choose core libraries and application frameworks.

  • What is your team expertise? How quickly can they learn a new framework?
  • Are you tempted to use a new shiny library instead of an established one? TIP: For serious applications prefer established, well-documented frameworks. New ones often lack features and maturity and they change their APIs and core concepts often which will add a lot of maintenance work, slowing down development of features.
  • TIP: Most established frameworks like Angular, React and Vue are pretty comparable when it comes to their performance and features. The real differences are developer experience, how often you will need to refactor to new patterns and how easy it will be to hire talent in case you need to expand the team.
  • Observation: React is known for being easier to start with than Angular, and you should be able to find developers familiar with it more quickly. On the other hand Angular used to have more built-in features and its own, opinionated way of developing apps, so onboarding an experienced Angular dev to the project may be quicker than if it was written in one of the dozens flavors of the React ecosystem.
  • Don't just add hundreds of libraries for every piece of functionality. NPM packages are developed by "the people" and are open-source, so always review them carefully before using them for your core business logic.
⚠️ Once a project reaches a certain scale it is very costly (and sometimes impossible) to change the application framework. You are likely making a choice that you won't be able to revert.

Choose or develop a UI system.

  • Choosing a UI library may result in the need to stick to its own way of composing the interface.
  • Carefully check the capabilities and development practices of the library that you are considering. Does it support all the features you need? Is it easy to extend? Does it work well with your application framework? Is it actively maintained? Does it support different ways of styling and theming?
  • If you have a designer/design team, the library should be agreed with them
  • Consider a project like shadcn, which contains meta-components that can be replicated with multiple different libraries.
⚠️ Once a project reaches a certain scale it is very costly (and sometimes impossible) to change the UI library. You are likely making a choice, that you won't be able to revert.

Choose a CMS (Content Management System) if needed.

  • The choice of your CMS may affect the app's architecture and coding language that you'll use.
  • A Headless CMS is separated from your app which takes away the issue above, but may incur additional costs and not having your core data on your own servers.

🗄️ Data model, application state and persistence

  • Identify the core entities, like users, products, etc. They will be related to the domains of your application. Most likely, they will mimic the entities on the backend, but that doesn't mean they'll be the same shape.
  • Think about persisting entities and state on different levels. State can be local to components, shared between them within a context of one module, or application-wide.
  • State that is shared between components or application-wide can create synchronization problems.
  • Application-wide state can become hard to manage, so try to limit it. There should be only one source of truth for shared state (a central place for updates) and changes should propagate automatically to every component that cares about them.
  • Persisting data between sessions should be done using well-known standards. For client-persisted data, use web APIs, like session storage and local storage. For the backend persisted data, keep to web standards, consider optimistic updates and other techniques that will make the UI nicer to the user.

🤝 API Design

  • List the endpoints that you'll likely need.
  • Keep endpoints structure and naming conventions according to REST rules.
  • Consider alternatives to the standard REST model, especially GraphQL for some types of applications. GraphQL will add complexity, require additional libraries and can make your application harder to maintain. On the other hand, it can result in less data being shared between the client and the server (faster requests, less bandwidth used).
  • Authentication model (if required), usage of SSL, CORS rules
  • Use standard HTTP status codes and handle failures

👩‍💻 Developer experience, CI/CD, testing strategy and monitoring

  • Repository structure - consider a monorepo if you have more products or backend + frontend written in the same programming language and want to move fast. A good example is having the server and client apps written in JavaScript. It can also be a good option for multiple client apps that share some common components and tooling.
  • Code style and static typing - linting rules, usage of TypeScript, prettier, and automations for enforcing rules, like githooks or CI workflows
  • Testing strategy - depending on the maturity of the project, it may be just unit tests, or a full suite of different types of tests using multiple frameworks and environments.
  • CI/CD - automated workflows, checks and how they integrate into the code merging process
  • Git strategy - branch naming, workflows, automated deployment on envs, how to merge hotfixes, etc.
  • Dependency management strategies - how and when to update core dependencies. Consider using bots for patch updates, like Dependabot or Renovabot.
  • Automated bundling and related tooling - consider Webpack, RSpack, Turbopack, Bun, and others. Consider using starter projects like Vite.
  • Monitoring errors and user sessions, for example with Sentry or LogRocket.
  • Monitoring code health, for example with Sonarcloud.

🚀 Performance and optimizations

  • Strategy for bundling the code in production and deciding what resources should be loaded at what time.
  • Consider Web Vitals as a good metric for your app's performance.
  • Make your output code small - minification, tree-shaking, lazy-loading, utilizing shared libraries.
  • Resource caching, CDN strategy.
  • Optimizing and reducing network requests.
  • Media optimization workflows may be baked in a CMS.
  • Know your frameworks and how they can be optimized on different levels - code, network requests, caching, etc.
  • Accessibility - semantic html, alt tags, aria-labels, media descriptions, color contrast, auditing and testing accessibility.

⚙️ The deployment infrastructure

This will not always be the Frontend architect's competence, but depending on the organization structure, he will usually be included in this topic and have awareness of the solutions.

  • Think about different metrics: cost, ease of use, no-brainer implementation, customization options, vendor responsiveness in case of issues, SLAs, etc.
  • How easy and costly is it to scale your app up/down to adjust to the traffic?
  • There are good Frontend-oriented infrastructure platforms, like Netlify and Vercel. They are easy to set up, and to automate deployments but may incur higher costs, less customization options, and even stir you to use a specific application framework (ex. Vercel excels with Next.jS, and Netlify is great with Gatsby).
  • Research the current state of the market. Maybe more established and simply-priced platforms like Heroku or DigitalOcean will be the best fit.
  • In case you have the time and budget, consider an enterprise solution, like AWS, Azure or GCP. Sometimes they may be a good fit for very specific workflows that can be integrated into your infrastructure (ex. media upload and processing, serving static objects, etc.).
  • If you like to tinker with Linux, consider a VPS (Virtual Private Server) - you will need to set up and maintain on your own server, but it can be a good alternative to avoid infrastructure lock-in and do things the way you want + keep the costs low.

For established products: Migrations at scale

A big part of maintaining mature applications is performing periodical migrations. If the app you are working on is successful, there will come a time when big migrations and paying off tech debt will become big headaches. You'll need to tackle them regularly in order to maintain development speed in other areas, like creating new features and reacting to customer's requests.

  • Plan your migrations. Analyze the impact and scope before jumping into refactoring multiple services. Identify the bottlenecks and spike changes on a representative case.
  • Zero-downtime. In a matured product you don't want to make a migration that will cause some parts of the app to fail.
  • Avoid big-bang releases. If you have to migrate a library or service that is used in multiple modules of your app (or different microfrontends), do it in an incremental way - for the time of migration, the old service will need to work alongside the new one.
  • Familiarize yourself with architectural patterns that help with migrations, like Strangler Fig pattern or Anti-corruption Layer.
  • Wrap important 3rd party libraries in your own service layer, where you control the API - you can then treat the library as an implementation detail that can be changed behind the scenes.
  • Automate what you can - the 80/20 rule states that in a larger migration there is a high proportion of changes that fit into a common template. Oftentimes there is a lot of redundant, easy to do "find-replace" type of work. All of the common patterns should be captured and automated (ex. in codemods), and the manual effort should be focused for the unusual cases.
  • Roll back fast if things fail - this involves different techniques, like chunking work into smaller releases or using config to quickly switch the versions of rolled out services and modules. Never leave yourself in a position where you can't roll back easily.

Common general pattern for migrations:

  • Wrap the target functionality with a developer-controller API and routing layer - you will switch implementations behind this layer
  • Update the places that use this functionality to use the wrapper layer instead of the concrete implementation
  • Start changing the implementation in the wrapper gradually.
  • Roll-out new versions of the wrapper where it is used
  • Cleanup - once the migration is complete. ⚠️ Don't skip this step! Leftovers from migrations become tech debt.

I hope list will be a good starting point for Frontenders for thinking about web apps architecture.