Most frontend codebases do not collapse suddenly. They slow down. Each sprint, features take a little longer, bugs appear in unrelated places, and developers become more cautious about touching certain files. By the time the team says "we need a rewrite", the problems were visible for a year.
These are the architectural mistakes I see most often when reviewing frontend applications — and what to do instead.
Mistake 1: No agreed structure
When every developer organises code their own way, the codebase becomes a collection of personal styles. Finding things gets harder, and duplication grows because nobody can find the existing helper.
Instead: agree on a structure — feature-based folders work well at scale — document it in one page and enforce the important rules with linting (for example, import restrictions between features). Consistency beats cleverness.
Mistake 2: Giant components
A 1,200-line component that fetches data, manages ten pieces of state, handles validation, formats values and renders three different layouts is hard to read, test and change safely.
Instead: split by responsibility:
- Data fetching and orchestration in a container or hook.
- Presentation in smaller components that receive data through props.
- Pure logic — formatting, calculations, validation — in plain functions you can unit test.
A useful signal: if you cannot describe a component's job in one sentence without "and", it is probably doing too much.
Mistake 3: Premature abstraction
The opposite problem is just as damaging. A developer sees two similar screens and builds a highly configurable GenericPage with thirty props and flags. The third screen is slightly different, so another flag is added. A year later, nobody understands the abstraction and every change risks breaking every page.
Instead: tolerate a little duplication until the pattern is clear. The common guidance — abstract on the third occurrence, not the first — exists for good reason. Prefer composition (small pieces combined) over configuration (one piece with many options).
Mistake 4: Everything in global state
Putting all data into a global store feels organised at first. Over time, it becomes a shared mutable space where any part of the app can change anything, and a small update triggers re-renders across the whole application.
Instead: give each kind of state an appropriate home:
- Server data in a caching layer designed for it.
- Filters, pagination and selection in the URL.
- Form values in the form.
- Truly global client concerns — session, theme, feature flags — in a small global store.
Global state should be the exception you justify, not the default.
Mistake 5: Components that talk directly to the API
When components call fetch directly with hard-coded URLs and parse raw responses inline, every backend change becomes a hunt across the codebase. Error handling, authentication and retries are implemented inconsistently.
Instead: introduce a thin API layer with typed functions, central error handling and mapping from backend responses to frontend models. Components ask for "orders for this customer", not for a URL.
Mistake 6: A design system that is not really a system
Many teams start a component library, then bypass it whenever it is inconvenient. Soon there are several button styles, inconsistent spacing and slightly different modals. Visual inconsistency is the visible symptom; the hidden cost is that every UI change must be made in many places.
Instead:
- Define design tokens (colours, spacing, typography, radii) as variables.
- Build a small set of well-designed primitives and make them easy to use.
- Treat changes to shared components as reviewed, versioned changes.
- Make the right thing the easy thing — if developers bypass the library, find out why.
Mistake 7: Ignoring loading, empty and error states
Designs often show the happy path only. Developers add a spinner and move on. Users then experience blank screens, layout jumps, infinite spinners after failed requests and cryptic errors.
Instead: design every data-driven view with four states — loading, empty, error and success — and build shared components for them so every screen handles them consistently.
Mistake 8: No boundaries between features
Feature A imports an internal helper from feature B. Feature B imports a component from feature C. Eventually, changing anything requires understanding everything, and circular dependencies appear.
Instead: each feature exposes a deliberate public interface, and everything else is private. Shared needs move into a shared layer that does not depend on any feature. Tools such as ESLint's import rules or dependency-cruiser can enforce these boundaries automatically.
Mistake 9: Treating performance as a later problem
"We will optimise later" often means "we will discover later that the architecture makes optimisation expensive". Loading everything on first page view, rendering unbounded lists and choosing heavy dependencies are hard to undo once the application is large.
Instead: set basic guardrails from the start — route-level code splitting, a bundle-size budget in CI, virtualisation for large lists, and a quick review before adding large dependencies.
Mistake 10: Tests that make change harder
Tests that assert internal implementation details — which function was called, the exact component structure — break on every refactor, so developers stop trusting them or stop refactoring.
Instead: test behaviour the user can observe. A small number of meaningful end-to-end tests on critical flows, integration tests for features, and unit tests for pure logic usually provide far more confidence than high coverage of implementation details.
Mistake 11: Architecture decisions that live in one person's head
When the person who designed the system leaves, the reasons behind decisions go with them. New developers either follow patterns they do not understand or unknowingly break them.
Instead: keep lightweight Architecture Decision Records, a short README per feature where it is helpful, and a living "how we build frontend here" guide. Documentation does not need to be long; it needs to exist.
How to recover an application that already has these problems
A rewrite is rarely the right first move. A safer sequence:
- Assess — identify the areas that change most often and hurt most.
- Set rules for new code — structure, state, API layer, component library — so the problem stops growing.
- Refactor incrementally — improve areas when you are already working in them, with tests around the behaviour first.
- Measure progress — build times, bundle size, bug rates, time to deliver typical features.
Scalable architecture is less about choosing the perfect pattern and more about consistently avoiding these traps. If your team feels the codebase slowing down and you want an objective view of where the real problems are, an architecture review can turn a vague feeling into a concrete, prioritised plan.