Welcome back. In our previous lesson, we established a framework for choosing between a Single-Page Application (SPA) and a Multi-Page Application (MPA), a high-level decision about an application's lifecycle. Now, we will delve deeper into the application's internal structure.
Today's lesson focuses on one of the most fundamental principles of scalable software development: separation of concerns. We will explore how to architect a layered frontend system. Your experience with React has likely exposed you to components that handle state, data fetching, and rendering all at once. This lesson will provide a systematic approach to untangle those responsibilities into distinct, manageable layers: UI, application state, domain logic, and data services. By the end, you'll be able to design a frontend architecture that is more maintainable, testable, and easier for teams to collaborate on.
The Problem: The Overloaded Component
As applications grow, a common anti-pattern emerges: the "god component." This is a single component that knows too much and does too much. It fetches data, manages complex state, contains business logic, and handles rendering.
This mixing of concerns creates several problems:
- Low Readability: It's difficult to understand what the component does because business logic is tangled with UI markup and data fetching side effects.
- Difficult to Maintain: A small change in one area (e.g., an API endpoint) can have unforeseen consequences in another (e.g., UI rendering), leading to bugs.
- Poor Reusability: Logic is tightly coupled to a specific presentation, making it nearly impossible to reuse in other parts of the application.
- Hard to Test: You cannot test the business logic without also rendering the component and mocking its many dependencies.
The goal of a layered architecture is to solve these problems by assigning each piece of code a single, clear responsibility.

The Layers of a Frontend Application
While there are many ways to slice an application, a robust layered architecture for a modern frontend application typically includes these four distinct areas of responsibility:
- UI / Presentation Layer: This is the most visible layer. Its sole responsibility is to render the user interface based on data it receives and to emit user events (like clicks or form submissions). In React, these are your components, which should ideally be as "dumb" as possible, receiving data via props and communicating changes via callback functions.
- Data / Service Layer: This layer handles all communication with the outside world. This includes fetching data from APIs, interacting with browser storage, or connecting to WebSockets. It encapsulates the specifics of how data is fetched (e.g., using
fetchoraxios, the exact URL endpoints, request headers) and provides a clean interface to the rest of the application. - Domain Logic Layer: This is the core of your application. It contains the business rules, data structures, and logic that are unique to your application's domain. For example, in an e-commerce app, this layer would define what a
ProductorOrderis and include logic for calculating totals or validating a shopping cart. This logic should be pure and have no knowledge of the UI or how data is fetched. - Application / State Layer: This layer acts as the orchestrator. It connects the UI layer with the domain and data layers. It manages the application's state, triggers data fetching when needed, and contains logic that is specific to the user's workflow but isn't core business logic. In modern React, custom hooks are the primary tool for implementing this layer.
A Practical Refactoring Journey
Theory is useful, but the best way to understand layering is to see it in action. We'll follow a practical refactoring process, starting from a single messy component and progressively separating it into clean layers. The following article by Alex Kondov provides an excellent, step-by-step example.
Clean Architecture in React | Alex Kondov
This article provides a complete walkthrough of refactoring a React component into a layered architecture. It's a great practical guide that mirrors the principles we're discussing.
First, read the initial section to understand the "god component" problem we start with. Then, follow the refactoring process through these key steps: Separating the UI Layer: Read the sections on Layout and The Page. Notice how generic layout and specific UI chunks (AnswersList, AnswerForm) are extracted, making the main page component responsible only for composition. Creating the Data Layer: Read Fetching Data. Pay close attention to how the fetch call is moved into a separate prompt-client.ts file. This creates our Data/Service layer. Creating the Application Layer: Read the rest of the "Fetching Data" section and the section on custom hooks. This is the most critical step. See how the usePrompt custom hook is introduced to encapsulate all the data fetching logic, state management (isLoading, isError), and event handling (handleSubmit). The component becomes incredibly simple and declarative.
The refactoring journey in Kondov's article demonstrates how to achieve a clean separation. The final PromptPage component is a perfect example of a thin UI Layer. The promptClient is the Data Layer. The usePrompt hook is the Application Layer, orchestrating the other two.
Deepening the Layers: Domain Models and Data Transformations
The approach above gives us a huge improvement in structure. We can take it even further by formalizing our Domain Layer and being more disciplined about our Data Layer.
Formalizing the Domain Layer
In the previous example, the "domain" was implicitly defined by the shape of the prompt object. For more complex applications, it's beneficial to create explicit domain models using classes or factory functions. These models can encapsulate business logic that is independent of any framework.
The article "Modularizing React Applications with Established UI Patterns" provides a great example of this.
Modularizing React Applications with Established UI Patterns
This article by Juntao QIU on martinfowler.com walks through a similar refactoring, with a strong emphasis on creating true domain models.
First, quickly skim the initial implementation of the Payment feature. Then, read the section on Data modelling. Notice how a PaymentMethod class is created. This class isn't just a data container; it has logic like the isDefaultChecked getter. This is a pure domain model. Finally, to see the power of this approach, read about handling a new requirement in the section Polymorphism to the rescue. By using a CountryPayment class (the Strategy pattern), complex, country-specific business logic is encapsulated cleanly within the domain layer, keeping the hooks and components simple.
Refining the Data Layer
Our Data Layer shouldn't just make the API call; it should be an "Anti-Corruption Layer." Its job is to protect our application from the messy and ever-changing details of external APIs. This means the data layer is responsible for all data transformations.
This video from Profy dev explains this concept perfectly.
Path To A Clean(er) React Architecture - API Layer & Data Transformations
This video demonstrates how to move data transformation and preparation logic out of your components and into the API layer.
Watch these two key segments: Transforming Responses: Watch from this segment to see how messy API response structures (response.data, response.included) are transformed into clean, application-friendly data structures inside the fetch function. Preparing Requests: Watch from this segment to see how preparing data for an upload (creating a FormData object) is moved into the API layer. The component just needs to pass the raw file.
By applying these principles, the rest of your application—the components and hooks—never has to know about the specific structure of an API's response or the format of its request bodies. If the API changes, you only need to update the data layer.
The Big Picture: Clean and Hexagonal Architecture
The layered approach we've discussed is a practical application of well-established software architecture patterns like Clean Architecture and Hexagonal Architecture (Ports and Adapters). These patterns emphasize a core of pure business logic with dependencies pointing inwards, away from frameworks and external details.

While a full-blown implementation of these patterns can be overkill for many frontend projects, understanding the core idea is incredibly valuable for making good architectural decisions.
Every Frontend Architecture Pattern Explained in 23 Minutes
The aformentioned video provides a concise overview of how these classical patterns relate to frontend development and offers a crucial reality check.
Please watch from this section. It explains how the core idea of a stable domain is central to these patterns and discusses the practical trade-offs of applying them strictly in a frontend context, where frameworks are already highly opinionated.
The key takeaway is not to rigidly implement "Clean Architecture" in every project, but to use its principles as a guide: keep your core logic pure and separate it from the details of the framework and external services.
Conclusion
In this lesson, we moved from the high-level structure of an application to its internal organization. We established the importance of separating concerns to build scalable and maintainable frontend systems.
Here are the key takeaways:
- The Goal of Layering: To combat the "god component" by giving each piece of code a single responsibility, making the system easier to understand, test, and change.
- The Four Key Layers:
- UI/Presentation: Renders UI, emits events. (React Components)
- Data/Service: Manages all external communication and data transformations. (API clients)
- Domain Logic: Contains pure, framework-agnostic business rules and data models. (Classes, pure functions)
- Application/State: Orchestrates the other layers and manages application state. (Custom Hooks)
- Practical Implementation: The refactoring process involves extracting presentational components, abstracting data access into service modules, and using custom hooks to encapsulate state and application logic.
- Dependency Rule: Your UI depends on your application logic, which depends on your domain and data layers. The core business logic should not depend on the UI.
Now that we have a solid understanding of how to layer the application as a whole, our next lesson will zoom into the UI Layer. We will focus specifically on how to design effective and scalable component prop interfaces, ensuring that the building blocks of our user interface are flexible, type-safe, and easy to use.