Back to insights

Frontend engineering

How I Structure Scalable Frontend Applications

A practical approach to frontend architecture using feature ownership, typed boundaries, explicit async states, reusable UI foundations, and release-focused quality checks.

8 min readBy Abdulrahman Hares
Frontend ArchitectureReactNext.jsTypeScriptSoftware Engineering
01

Architecture starts with product boundaries

A scalable frontend is not created by adding more folders. It starts by understanding which parts of the product change together, who owns each workflow, and where data enters or leaves the interface. Before choosing a directory structure, I map the main user journeys, shared visual patterns, permission boundaries, and failure states.

This keeps architecture connected to real product behaviour. A hospital workflow, a legal case screen, and an AI analysis workspace may all use React, but they should not be organised as if they have the same responsibilities. The structure should make the business flow easier to find, change, review, and test.

  • Group code around product features rather than technical file types alone.
  • Keep cross-feature UI primitives small, stable, and intentionally generic.
  • Treat permissions, loading, empty, error, and success states as product requirements.
02

Give every feature clear ownership

Each feature should own the components, hooks, validation, types, queries, mutations, and state that exist only for that workflow. This reduces the number of unrelated files a developer must inspect before making a change and makes pull requests easier to review.

Shared code is valuable only when it represents a genuinely shared contract. Moving code into a global utility too early often creates hidden coupling. I prefer to keep behaviour close to the feature first, then extract it after repeated use proves that the abstraction is stable.

03

Use typed integration boundaries

Frontend screens should not depend directly on inconsistent backend responses. I use a reusable API client, typed request and response models, normalisation functions, and predictable error objects. This gives the interface one stable boundary even when an endpoint evolves or two services return data in different shapes.

Typed boundaries also improve communication with backend teams. Instead of discussing vague UI bugs, the team can point to a contract: required fields, nullable values, pagination metadata, validation errors, and mutation results.

  • Keep transport models separate from view models when the shapes serve different purposes.
  • Normalise errors before they reach components.
  • Represent pagination and filters explicitly instead of hiding them inside page components.
04

Design async states as part of the interface

Production applications spend a large amount of time between ideal states. Data is loading, a filter returns no results, a mutation fails, a token expires, or a user has insufficient permission. These are not edge decorations; they are part of the main experience.

I define the loading, empty, partial, stale, error, success, and retry behaviour while designing the feature. Doing this early prevents each screen from inventing a different pattern and makes operational systems feel more reliable.

05

Release quality is an architectural concern

A maintainable codebase still fails if the release process ignores responsiveness, accessibility, performance, analytics, and real device behaviour. I include those checks in the definition of done and review them before a feature reaches production.

The result is not a perfect architecture that never changes. It is a system where change has an understandable cost, ownership is visible, and the next developer can continue without rebuilding the product from zero.

Related project work

Case studies connected to the decisions in this article.

Continue reading

Related engineering notes

View all