Skip to content

A backend template using FastAPI, designed to be adaptable and framework-agnostic. Implements Clean Architecture and DDD principles with modular layer separation and dependency inversion for maintainability. Includes session-based authentication with tokens in cookies to manage session IDs and a simple user permissions management system.

License

Notifications You must be signed in to change notification settings

iq1/fastapi-clean-example

 
 

Repository files navigation

Table of contents

  1. Overview
  2. Architecture Principles
    1. Introduction
      1. Note on Adapters
    2. Layered Approach
    3. Dependency Rule
    4. Dependency Inversion
    5. Dependency Injection
  3. Project
    1. API
    2. Architecture
    3. Structure
      1. Scenarios?
    4. Technology Stack
    5. Configuration
      1. Flow
      2. Setup
      3. Launch
  4. Acknowledgements
  5. Todo

Overview

This FastAPI-based project and its documentation represent my interpretation of Clean Architecture principles with subtle notes of Domain-Driven Design (DDD). While not claiming originality or strict adherence to every aspect of these methodologies, the project demonstrates how their key ideas can be effectively implemented in Python.

The vision behind the project was shaped with insights from the ASGI Community Telegram chat. For a deeper understanding of the concepts applied here, the following resources are recommended:

Architecture Principles

Introduction

This repository may be helpful for those seeking a backend implementation in Python that is both framework-agnostic and storage-agnostic (unlike Django). Such flexibility can be achieved by using a web framework that doesn't impose strict software design (like FastAPI) and applying a layered architecture patterned after the one proposed by Robert Martin, which we'll explore further.

Clean Architecture Diagram
Figure 1a: Robert Martin's Clean Architecture Diagram

The original explanation of the Clean Architecture concepts can be found here. If you're still wondering why Clean Architecture matters, read the article — it only takes about 5 minutes. In essence, it’s about making your application independent of external systems and highly testable.

Note on Adapters

In my opinion, the diagram by R. Martin in Figure 1a can, without significant loss, be replaced by a more concise and pragmatic one — where the adapter layer depends both on the internal layers of the application and external components. This adjustment implies reversing the arrow from the blue layer to the green layer.

The proposed solution is a trade-off: it doesn't strictly follow R. Martin's original concept but avoids introducing excessive abstractions with implementations outside the application's boundaries. Pursuing purity on the outermost layer, as envisioned by R. Martin, is more likely to result in overengineering than in practical gains. My approach retains nearly all the advantages of Clean Architecture while simplifying real-world development. When needed, adapters can be removed along with the external components they're written for, which isn't a significant issue.

Let's agree, for this project, that Clean Architecture focuses on managing dependencies within the application's boundaries.

Revised Clean Architecture Diagram
Figure 1b: Revised Robert Martin's Clean Architecture Diagram

Layered Approach

  • #gold Domain Layer. This is the core of the business logic, defining the ubiquitous language for the entire application. It contains entities, value objects, and domain services that encapsulate business rules. By establishing consistent terminology, it ensures clear communication and alignment with the business domain. Fully isolated from external systems, the Domain layer remains the most stable and least prone to change.

    Note: The Domain layer may also include repository interfaces (abstractions for storing and retrieving domain entities) and aggregates (groups of entities and value objects that set consistency boundaries and ensure domain rules are followed). These concepts aren't implemented in the project's codebase, but they're worth exploring to better understand DDD.

  • #red Application Layer. This layer bridges business requirements and implementation details. It defines and coordinates use cases — high-level abstractions describing business scenarios. Each use case encapsulates rules and flows needed to achieve a business goal.

    The core component of this layer is the interactor, representing individual step within a use case. Interactors orchestrate business logic by applying domain rules and delegating tasks to domain entities and services. To access external systems, interactors use interfaces (ports), which abstract infrastructure details.

    Interactors can be grouped into an application service, combining actions sharing a close context.

  • #green Infrastructure Layer. This layer provides implementations (adapters) for the interfaces (ports) defined in the Application layer, enabling interaction with external systems like databases, APIs, and file systems while keeping the core business logic isolated.

    Related adapter logic can also be grouped into an infrastructure service if justified.

  • #blue Presentation Layer. This layer handles external requests and includes controllers that validate inputs and pass them to the interactors in the Application layer. Controllers must be as thin as possible and ideally contain no logic beyond basic input validation and routing. Their role is strictly to act as a boundary between the application and external systems (e.g., FastAPI).

    Note: In the original diagram, the Presentation layer isn't distinguished and is included within the Interface Adapters layer. I introduce it as a separate layer, marked in blue, as I view it as even more external compared to typical adapters.

Important

  • Basic validation (e.g., type safety, required fields, input format) should be performed in controllers at this layer, while business rule validation (e.g., ensuring the email domain is allowed, verifying the uniqueness of a username, or checking that a user meets the required age) should be handled in the Domain layer.
  • In the Domain layer, validation often checks how multiple fields relate to each other. For example, ensuring that a discount applies only within a specific date range, or that a promotion code can only be used with orders above a certain total amount.
  • Pydantic is entirely useless in the Domain layer. Its serialization features have no relevance here, and while it might seem suitable for validation, its capabilities are limited to simple checks like types and formats. Domain validation, on the other hand, requires enforcing complex relationships and business-specific rules, tasks that Pydantic cannot handle.
  • #gray External Layer. This layer represents fully external components such as web frameworks (e.g. FastAPI itself), databases, third-party APIs, and other services. These components operate outside the application’s core logic and can be easily replaced or modified without affecting the business rules, as interactions occur through the Presentation and Infrastructure layers.

    Note: In the original diagram, the external components are included in the blue layer (Frameworks & Drivers). Here, I've marked them in gray to clearly separate them from the layers within the application's boundaries.

My Interpretation of CAD My Interpretation of CAD, alternative

Figure 1c: My Pragmatic Interpretation of Clean Architecture Diagram
(original and alternative representation)

Important

  • Clean Architecture doesn't prescribe any particular number of layers. The key is to follow the Dependency Rule, which is explained in the next section.

Dependency Rule

A dependency occurs when one software component relies on another to operate. Typically, dependencies are graphically depicted in UML style in such a way that

Important

  • A -> B (A points to B) means A depends on B.

Or in terms of modular code, where different components live in separate modules:

  • if module A imports module B, then A depends on B.

The key principle of Clean Architecture is the Dependency Rule. This rule states that dependencies never point outwards within the application's boundaries, meaning that the more abstract layers of software must not depend on more concrete ones. As we have already agreed, this does not apply to adapters connecting the application to external systems, therefore, adapters can serve as a bridge between the internal architecture and the External layer.

Basic Dependency Graph
Figure 2: Basic Dependency Graph

In the diagrams, the Domain layer, being the most abstract one, remains independent and ensures a stable core.

Important

  • Components within the same layer can depend on each other. For example, components in the Infrastructure layer can interact with one another without crossing into other layers.

  • Components in any outer layer can depend on components in any inner layer, not necessarily the one closest to them. For example, components in the Presentation layer can directly depend on the Domain layer, bypassing the Application and Infrastructure layers.

  • However, avoid letting business logic leak into peripheral details, such as raising business-specific exceptions in the Infrastructure layer or enforcing domain rules outside the Domain layer.

Dependency Inversion

The dependency inversion technique enables reversing dependencies by placing an interface between the components, allowing the inner layer to communicate with the outer while following the Dependency Rule. By doing so, architectural violations are avoided, preserving the integrity of the layered design.

Corrupted Dependency
Figure 3a: Corrupted Dependency

In this example, the application directly depends on the infrastructure, which violates the Dependency Rule. The inner layer (Application) should not be coupled to the outer layer (Infrastructure), as this increases the system's rigidity and makes it more prone to issues. Such coupling leads to "corrupted" dependencies, making the code harder to maintain and extend. Any changes in the Infrastructure layer can unintentionally affect the Application layer.

Correct Dependency
Figure 3b: Correct Dependency

In the correct design, the Application layer depends on an abstraction (port), and the Infrastructure layer implements the corresponding interface. The infrastructure component acts as an adapter to the port, maintaining separation of concerns and following the Dependency Inversion principle. This design ensures that the application remains decoupled from the infrastructure, allowing changes in the Infrastructure layer with minimal impact on the core business logic.

Dependency Injection

The idea behind Dependency Injection is that a component should not create the dependencies it needs, but rather receive them from the outside. From this definition, it's clear that the implementation of DI is often closely tied to the __init__ method and function signatures, where the required dependencies are passed in.

But how exactly should these dependencies be passed?

DI frameworks offer an elegant solution by automatically creating the necessary objects (while managing their lifecycle) and injecting them where needed. This makes the process of dependency injection much cleaner and easier to manage.

Correct Dependency with DI
Figure 3c: Correct Dependency with DI

FastAPI includes a built-in DI mechanism called Depends. However, applications designed in line with Clean Architecture principles shouldn't be tightly coupled to any particular web framework. Since the web framework is the outermost layer of the application, it should be easily replaceable. Refactoring the entire codebase to remove Depends if you switch frameworks is far from ideal. It's much more convenient to have a DI solution that works with any web framework. Personally, I prefer Dishka.

Project

Architecture

Detailed project architecture
Figure 4: Project architecture

This diagram shows the architecture of the project, demonstrating how components interact in line with Clean Architecture principles. It illustrates how the Dependency Rule and Dependency Inversion Principle are applied to keep the core business logic independent of external systems, while allowing flexible integration with infrastructure and external services.

Structure

.
├── ...
├── .env.example    # example env vars for Docker/local dev
├── config.toml     # primary config file
├── Makefile        # shortcuts for setup and common tasks
├── pyproject.toml  # tooling and environment config
├── scripts/...     # helper scripts
└── src/
    └── app/
        ├── run.py              # app entry point
        ├── application/...     # Application layer
        ├── domain/...          # Domain layer
        ├── infrastructure/...  # Infrastructure layer
        ├── presentation/...    # Presentation layer
        ├── scenarios/...       # specific scenarios (e.g., create_user, log_in, etc.)
        └── setup/
            ├── app_factory.py  # app builder
            ├── config/...      # app settings
            └── ioc/...         # dependency injection setup

Scenarios? Which layer they belong to?

Scenarios
Figure 5: Scenarios

What you see in scenarios/ is the upper part of the project architecture shown earlier, organized into vertical slices that combine components from the Presentation and Application layers. Each slice reflects a specific task, such as create_user or log_in. For me, this is simply a practical way to structure the project, not part of the architecture itself, though it does resemble Robert Martin’s concept of "screaming architecture".

Technology Stack

  • Python: 3.12
  • Core: alembic, alembic-postgresql-enum, bcrypt, dishka, fastapi, orjson, psycopg3[binary], pydantic[email], pyjwt[crypto], rtoml, sqlalchemy[mypy], uuid6, uvicorn, uvloop
  • Testing: coverage, pytest, pytest-asyncio
  • Development: bandit, black, isort, line-profiler, mypy, pre-commit, pylint, ruff

API

Handlers
Figure 6: Handlers

General

  • /: Open to everyone.
    • Redirects to Swagger Documentation.
  • /api/v1/: Open to everyone.
    • Returns 200 OK if the API is alive.

Auth (/api/v1/auth)

  • /signup: Open to everyone.
    • Registers a new user with validation and uniqueness checks.
    • Passwords are peppered, salted, and stored as hashes.
  • /login: Open to registered users.
    • Authenticates a user, sets a JWT access token with a session ID in cookies, and creates a session.
    • A logged-in user cannot log in again until the session expires or is terminated.
    • Authentication renews automatically when accessing protected routes before expiration.
    • If the JWT is invalid, expired, or the session is terminated, the user loses authentication.
  • /logout: Open to authenticated users.
    • Logs the user out by deleting the JWT access token from cookies and removing the session from the database.

Users (/api/v1/users)

  • / (POST): Open to admins.
    • Creates a new user, including admins, if the username is unique.
  • / (GET): Open to admins.
    • Retrieves a paginated list of existing users with relevant information.
  • /inactivate: Open to admins.
    • Soft-deletes an existing user, making that user inactive.
  • /reactivate: Open to admins.
    • Restores a previously soft-deleted user.
  • /grant: Open to admins.
    • Grants admin rights to a specified user.
  • /revoke: Open to admins.
    • Revokes admin rights from a specified user.

Note: Endpoints /inactivate, /reactivate, /grant, and /revoke are fully functional but should be reworked if the system requires superuser control. Currently, admins can manage other admins.

Note: The initial admin privileges must be granted manually (e.g., directly in the database), though the user account itself can be created through the API.

Configuration

Flow

Warning

  • This part of documentation is not related to the architecture approach. You are free to choose whether to use the proposed automatically generated configuration system or provide your own settings manually, which will require changes to the Docker configuration. However, if settings are read from environment variables instead of config.toml, modifications to the application's settings code will be necessary.

Important

  • In the configuration flow diagram below, the arrows represent the flow of data, not dependencies.

Configuration flow (toml to env)
Figure 7: Configuration flow (.toml to .env)

  1. config.toml: primary config file
  2. .env: derivative config file which Docker needs

Configuration flow (app)
Figure 8: Configuration flow (app)

Setup

1. Primary configuration

Edit the config.toml file to set up primary config.

Warning

Don't rename existing variable names or remove comments unless absolutely necessary. This action may invalidate scripts associated with the Makefile. You can still fix them or not use Makefile at all.

2. Derivative configuration

Generate .env file in one of the ways:

  1. Safe, as long as config.toml is correct

    • make dotenv

    For this method you must manually set the correct value of POSTGRES_HOST variable in config.toml. Its value will be localhost for local launch, or the name of the Docker service from docker-compose.yaml.

  2. Less secure, but very convenient

    • make dotenv-docker

    to prepare .env for Docker environment, or

    • make dotenv-local

    for local dev purposes, such as a locally hosted database.

    Under the hood, the corresponding variable in config.toml becomes uncommented, then the script associated with make dotenv is called.

  3. Unsafe: Rename .env.example to .env and check if all variables are correct.

Launch

Database only

One downside of this launch method is that it automatically attempts to rewrite config.toml to create .env.

  • To run only the database in Docker and use the app locally, use the following command:

    • make up-local-db
  • Then, apply the migrations with:

    • alembic upgrade head

    After applying the migrations, you can start the application locally as usual. The database is now set up and ready to be used by your local instance.

  • To stop the database container, use:

    • make down
  • To completely remove the database along with the applied migrations, run:

    • docker compose down -v

All containers

  • After completing both steps in Setup, you can launch all containers. Choose one of the following commands:

    • docker compose up --build
    • make up-echo
  • To run containers in the background, choose one of the following commands:

    • docker compose up --build -d
    • make up
  • To stop the containers, choose one of the following commands:

    • docker compose down
    • make down

Feel free to take a look at Makefile, it contains many more useful commands.

Acknowledgements

I would like to express my sincere gratitude to the following individuals for their valuable ideas and support in satisfying my curiosity throughout the development of this project:

I would also like to thank all the other participants of the ASGI Community Telegram chat for their insightful discussions and shared knowledge.

Todo

  • simplify the configuration
  • switch from poetry to uv
  • explain the code in README
  • increase test coverage and set up CI

About

A backend template using FastAPI, designed to be adaptable and framework-agnostic. Implements Clean Architecture and DDD principles with modular layer separation and dependency inversion for maintainability. Includes session-based authentication with tokens in cookies to manage session IDs and a simple user permissions management system.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python 95.7%
  • Shell 1.7%
  • Makefile 1.7%
  • Other 0.9%