"Frontend architecture" sounds abstract until you have lived without it. Then it becomes very concrete: a one-line change that breaks three unrelated screens, a state bug nobody can reproduce, a new developer who needs a month before they can ship safely, and a build that takes longer every sprint.
Architecture is simply the set of decisions that are expensive to change later. For a large web application, these are the ones that matter most.
Start with the constraints, not the tools
Before picking libraries, write down the forces acting on the application:
- Size and lifespan. Twenty screens for eighteen months, or two hundred screens for ten years?
- Team shape. One team, or several teams owning different areas?
- Users and devices. Internal users on desktops, or customers on mid-range phones over mobile data?
- Data characteristics. Mostly read-only content, or highly interactive, real-time, collaborative data?
- SEO and first load. Does a search engine need to see the page? Does first-load speed affect revenue?
- Integration surface. How many APIs and third-party services does the UI talk to, and how stable are they?
Most architecture mistakes are not bad choices in isolation. They are good choices for a different set of constraints.
Structure the code around features, not file types
A common starting structure groups by technical type: components/, services/, hooks/, utils/. It works for small projects. At scale, every feature is scattered across every folder, and nothing tells you what depends on what.
A feature-oriented structure scales better:
src/
app/ # bootstrapping, routing, providers
features/
orders/ # everything the Orders feature needs
api/
components/
hooks/
state/
index.ts # the feature's public surface
customers/
shared/
ui/ # design-system components, no business logic
lib/ # framework-agnostic helpersThe important rule is the dependency direction: features may use shared, but shared must never import from a feature, and features should talk to each other only through their public index.ts. A lint rule that enforces this is worth more than a long document explaining it.
Design components in layers
Not every component should be allowed to do everything. A practical layering:
- Primitives — buttons, inputs, modals, layout. No knowledge of the domain. This is your design system.
- Domain components —
OrderStatusBadge,CustomerCard. Know the business vocabulary, but receive data through props. - Containers / feature screens — fetch data, handle loading and error states, coordinate actions.
When a primitive starts importing API code, or a domain component starts fetching its own data, the layers blur and reuse becomes painful. Keep data fetching near the top and pass data down.
A shared component library pays for itself
Once more than one team builds UI, a shared component library — even a small one with buttons, form fields, tables and dialogs — prevents a slow drift into five slightly different date pickers. Pair it with design tokens (colours, spacing, typography as variables) so visual changes happen in one place.
Decide where state lives — explicitly
Most frontend complexity is state complexity. It helps to separate state into kinds, because each has a different best home:
| Kind of state | Examples | Usual home |
|---|---|---|
| Server state | Orders, products, user profile | A data-fetching/cache layer (e.g. TanStack Query, RTK Query, Angular services with signals) |
| URL state | Filters, pagination, selected tab | The URL — so it is shareable and survives refresh |
| Form state | Field values, validation | A form library, local to the form |
| Local UI state | Is this dropdown open? | The component itself |
| Global client state | Theme, feature flags, auth session | A small global store or context |
The most common anti-pattern I see is putting server data into a global client store and then hand-writing caching, refetching and invalidation logic. A dedicated server-state layer removes a large category of bugs.
Treat the API boundary as part of the architecture
The frontend should not know every quirk of every backend response. Put a thin API layer between the two:
- Typed request and response models (generated from OpenAPI or GraphQL schemas where possible).
- One place for authentication headers, retries and error normalisation.
- Mapping from backend shapes to the shapes your UI actually needs.
When a backend field is renamed, you change one mapper instead of forty components. If you control the backend too, a backend-for-frontend (BFF) endpoint that returns exactly what a screen needs can remove a lot of client-side orchestration.
Plan for performance from the start
You do not need to optimise everything on day one, but some decisions are hard to retrofit:
- Route-level code splitting so users download only the screens they visit.
- A rendering strategy per area: static or server-rendered for public pages, client-rendered for authenticated dashboards is a common and sensible split.
- Performance budgets in CI — for example, fail the build if the main bundle grows beyond an agreed size.
- Virtualised lists for any table that can reach thousands of rows.
Should you use micro-frontends?
Micro-frontends solve an organisational problem: multiple teams that need to deploy independently. They add real costs — duplicated dependencies, cross-app communication, version alignment and more complex tooling.
If you have one or two teams, a well-structured modular monolith with clear feature boundaries will almost always serve you better. Consider micro-frontends when team autonomy and independent release cycles are a genuine, current bottleneck, not a future possibility.
Write the decisions down
Architecture that lives only in one senior developer's head is a risk. Lightweight Architecture Decision Records (ADRs) — a short file per decision describing the context, the choice and the consequences — make the reasoning visible. When someone asks "why do we do it this way?" in two years, the answer exists.
A checklist for your next large application
- Constraints documented: size, lifespan, teams, users, SEO, integrations.
- Feature-based structure with enforced dependency rules.
- Component layers: primitives, domain components, containers.
- Explicit homes for server, URL, form, local and global state.
- A typed API layer between UI and backend.
- Code splitting and a performance budget in CI.
- Micro-frontends only when team autonomy truly requires them.
- ADRs for decisions that are expensive to reverse.
Good frontend architecture is rarely clever. It is a set of boring, consistent decisions that let many people change a large codebase safely. If you are about to start a large application — or you are already feeling the pain of one that grew without a plan — an architecture review early on is one of the highest-leverage investments you can make.