Skip to main content

Introduction

So far, we have organized our application into multiple files, such as routes and models, making the project easier to navigate. However, our route functions still perform multiple responsibilities. A typical endpoint may:
  • Receive the HTTP request.
  • Validate the incoming data.
  • Execute business logic.
  • Read or update data.
  • Return the HTTP response.
This approach works well for small applications, but as the project grows, route functions become larger, harder to maintain, and more difficult to test. To solve this problem, we separate the application into multiple layers, where each layer has a single responsibility. This design is known as the Controller–Service–Repository (CSR) Architecture or Layered Architecture.

Why Modularize?

Consider the following endpoint.
Creating an employee involves several responsibilities:
  1. Receiving the HTTP request.
  2. Validating the input.
  3. Applying business rules.
  4. Saving the employee.
  5. Returning the response.
Instead of performing all these tasks inside a single function, we distribute them across dedicated layers.

Before Modularization

Although this endpoint is simple, it performs multiple responsibilities:
  • Handles the HTTP request.
  • Executes business logic.
  • Accesses the data store.
  • Returns the HTTP response.
As more features are added, these route functions become longer and harder to maintain.

After Modularization

The same request is divided into dedicated layers.
Each layer focuses on a single responsibility.

Layer Responsibilities

This design follows the Single Responsibility Principle (SRP).

Benefits

A layered architecture makes the application:
  • Easier to understand
  • Easier to maintain
  • Easier to test
  • Easier to extend
  • Easier to debug
  • Easier to reuse
It also makes future changes much easier. For example:
  • Replacing the in-memory dictionary with PostgreSQL requires changes only in the Repository.
  • Adding new business rules requires changes only in the Service.
  • The Controller continues to expose the same HTTP APIs.

What We Will Build

In this chapter, we will gradually refactor our Employee Management System into a modular FastAPI application. By the end of this chapter, our Employee Management System will follow the same architecture used in most production FastAPI applications.

Step 1: Define the Models

Before implementing the Router, Service, and Repository layers, let’s define the models used by our Employee Management System. Each layer works with data in a different way, so instead of using a single model everywhere, we’ll define separate models for different responsibilities.
  • Request Models validate data received from clients.
  • Business Models represent the application’s working data.
  • Response Models control the data returned to clients.
Keeping these models in a separate module allows every layer of the application to reuse them.

Project Structure

Model Flow

Why Different Models?

Each model has a different responsibility. For example, when a client creates a new employee, they only send:
After validation, the application creates its own business object by adding internally generated information.
Before sending the response back to the client, only the required fields are exposed.
This separation keeps the application secure, flexible, and easy to maintain.

Task

Create the following file.
Define the Request, Business, and Response models.

Solution

What We Have So Far

Our application now has a clear separation between incoming data, internal processing, and outgoing data.
The Router will use the Request and Response models, while the Service and Repository will work with the Business model. In the next step, we’ll implement the Repository Layer, which will be responsible for storing and retrieving EmployeeBusiness objects.

Step 2: Repository Layer

The Repository is responsible for interacting with the application’s data source. It acts as a bridge between the Service Layer and the data source, hiding the implementation details of how data is stored or retrieved. At this stage, our data source is an in-memory dictionary. Instead of creating the data inside the Repository, we’ll inject it through the constructor. This technique is called Constructor Injection, one of the most common forms of Dependency Injection (DI). Later in this chapter, FastAPI’s Depends() will perform this injection automatically.

Project Structure

Responsibilities

The Repository is responsible for:
  • Reading employee data.
  • Creating new employees.
  • Updating existing employees.
  • Deleting employees.
  • Converting raw data into EmployeeBusiness objects.
The Repository should not:
  • Validate requests.
  • Apply business rules.
  • Return HTTP responses.

Constructor Injection

Instead of creating the data source itself, the Repository receives it from outside.
This keeps the Repository independent of where the data comes from.

Task

Create the following files.
Move the employee data into database.py and inject it into the Repository using the constructor.

Solution

Using the Repository

For now, we manually inject the data source while creating the Repository.
This is Constructor Injection because the dependency (EMPLOYEES) is supplied through the constructor rather than being created inside the Repository.

What We Have So Far

At this stage, the Repository depends on the data source through Constructor Injection. In a later step, we’ll replace this manual injection with FastAPI Dependency Injection (Depends), allowing FastAPI to create and inject the Repository automatically.

Step 3: FastAPI Dependency Injection

In the previous step, we manually created the Repository by passing the data source through its constructor.
This is Constructor Injection, where the dependency is supplied from outside the class. While this works, manually creating dependencies throughout the application becomes repetitive as the number of layers grows. FastAPI solves this problem using Dependency Injection with Depends(). Instead of creating objects ourselves, we describe how to create them, and FastAPI automatically creates and injects them whenever they are needed.

Dependency Flow

Task

Create a dependency provider that constructs and returns an EmployeeRepository.

Solution

Using the Dependency

Instead of creating the Repository manually:
we can now ask FastAPI to provide it.
RepositoryDep is now a reusable dependency that can be injected into the Service layer.

What We Have So Far

The Repository is no longer created manually throughout the application. Instead, FastAPI knows how to construct it whenever it is required. In the next step, we’ll build the Service Layer, which will receive the EmployeeRepository through dependency injection and implement the application’s business logic.

Step 4: Service Layer

The Service layer contains the application’s business logic. It acts as an intermediary between the Router and the Repository. Instead of directly accessing the Repository, the Router delegates the request to the Service, which applies business rules and coordinates data operations.

Project Structure

Responsibilities

The Service is responsible for:
  • Implementing business rules.
  • Coordinating Repository operations.
  • Creating Business Models from Request Models.
  • Returning Business Models to the Router.
The Service should not:
  • Handle HTTP requests or responses.
  • Read or write data directly.
  • Know how the data is stored.

Service Flow

Constructor Injection

The Service depends on the Repository. Instead of creating it internally, it receives the Repository through its constructor.
This keeps the Service loosely coupled to the Repository.

Task

Create the following file.
Implement the Service methods by using the Repository.

Solution

Register the Dependency

Create a dependency provider for the Service.

What We Have So Far

The Service now contains the application’s business logic while delegating all data access to the Repository. Notice that:
  • The Service works with Request Models and Business Models.
  • The Repository works only with Business Models.
  • Neither layer knows anything about HTTP requests or responses.
In the next step, we’ll build the Router Layer, which will receive HTTP requests, validate them using the Request Models, delegate the work to the Service, and return Response Models to the client.

Step 5: Router Layer

The Router is the entry point of every HTTP request. Its responsibility is to receive requests, validate the incoming data, delegate the work to the Service, and return the appropriate response. The Router should not contain business logic or data access code.

Project Structure

Responsibilities

The Router is responsible for:
  • Defining API endpoints.
  • Receiving HTTP requests.
  • Validating Request Models.
  • Calling the Service.
  • Returning Response Models.
The Router should not:
  • Generate employee IDs.
  • Apply business rules.
  • Access the data source directly.

Request Flow

Task

Create the following file.
Implement the Employee APIs by delegating all business operations to the Service.

Solution

Register the Router

Finally, register the Router with the FastAPI application.

What We Have So Far

The application is now organized into four independent layers:
  • Router handles HTTP requests and responses.
  • Service implements business logic.
  • Repository manages data access.
  • Database stores the application’s data.
Each layer has a single responsibility, making the application easier to understand, test, maintain, and extend. In the next step, we’ll trace the complete request lifecycle and see how FastAPI automatically creates and injects the required dependencies using Depends().