/fastapi-clean-example

Backend template using FastAPI, but framework-agnostic by design. Implements Clean Architecture and DDD patterns with clearly separated layers and dependency inversion. Includes session-based authentication using cookies and role-based access control with permissions and hierarchical roles.

Primary LanguagePythonMIT LicenseMIT

Table of contents

  1. Overview
  2. Architecture Principles
    1. Introduction
    2. Layered Approach
    3. Dependency Rule
      1. Note on Adapters
    4. Layered Approach Continued
    5. Dependency Inversion
    6. Dependency Injection
  3. Project
    1. Dependency Graphs
    2. Structure
    3. Technology Stack
    4. API
      1. General
      2. Account
      3. Users
    5. Configuration
      1. Flow
      2. Local Development
      3. Docker Deployment
  4. Useful Resources
  5. Acknowledgements

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. If they're new to you, refer to the Useful Resources section.

πŸ’¬ Feel free to open issues, ask questions, or submit pull requests.

⭐ If you find this project useful, please give it a star or share it!

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.

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.

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

"A computer program is a detailed description of the policy by which inputs are transformed into outputs."

β€” Robert Martin

The concentric circles represent boundaries separating layers, each with its own policies. The less likely a policy is to change, and the more abstract and independent of implementation it is, the closer it is to the center. An example of the least abstract policy is an I/O operation.

The meaning of the arrows in the diagram will be discussed later. For now, we will focus on the purpose of the layers.

Layered Approach

#gold Domain Layer

  • The core of the application, containing entities, value objects, and domain services that encapsulate critical business rules β€” fundamental principles or constraints that define how the business operates and delivers value. In some cases, these rules can be seen as mechanisms that create the product's value independently of its software implementation. Changing them often means changing the business itself.
  • It establishes a ubiquitous language β€” a consistent terminology shared across the application and domain. This is the language you can speak with managers.
  • It's the most stable and independent part of the application.

Note

The Domain layer may also include aggregates (groups of entities that must change together as a single unit, defining the boundaries of transactional consistency) and repository interfaces (abstractions for manipulating aggregates). While these concepts aren't implemented in the project's codebase, understanding them can deepen your knowledge of DDD.

#red Application Layer

  • This layer contains applied business logic, defining use cases β€” high-level abstractions that bridge the domain layer with its practical implementation, orchestrating business logic to achieve specific goals.
  • Its core component is the interactor, representing an individual step within a use case.
  • 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.

Note

Domain and Application layers may import libraries that extend the language's capabilities or provide general-purpose utilities (e.g., for numerical computations, timezone management, or object modeling). However, they should avoid any ties to specific frameworks, databases, or external systems.

#green Infrastructure Layer

  • This layer is responsible for adapting the application to external systems.
  • It provides implementations (adapters) for the interfaces (ports) defined in the Application layer, allowing the application to interact with external systems like databases, APIs, and file systems while keeping the business logic decoupled from them.
  • Related adapter logic can also be grouped into an infrastructure service.

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. If you were to split all blocks of code into separate modules, dependencies would manifest as imports between those modules. 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.

The key principle of Clean Architecture is the Dependency Rule. This rule states that more abstract software components must not depend on more concrete ones. In other words, dependencies must never point outwards within the application's boundaries.

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 declaring domain rules outside the Domain layer.

  • In specific cases where database constraints enforce business rules, the Infrastructure layer may raise domain-specific exceptions, such as UsernameAlreadyExists for a UNIQUE CONSTRAINT violation. Handling these exceptions in the Application layer ensures that any business logic implemented in adapters remains under control.

  • Beware of introducing elements in an inner layer tailored specifically to the needs of an outer layer. For example, you might be tempted to place something in the Application layer that exists solely to support a specific piece of infrastructure. At first glance, based on imports, it might seem that the Dependency Rule isn't violated. However, in reality, you've broken the core idea of the rule by embedding infrastructure concerns (more concrete) into the business logic (more abstract).

Note on Adapters

In my opinion, the diagram by R. Martin in Figure 1 can, without significant loss, be replaced by a more concise and pragmatic one β€” where the adapter layer serves as a bridge, depending 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 in R. Martin's diagram.

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 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 Dependency Rule does not apply to adapters.

My Interpretation of CAD My Interpretation of CAD, alternative

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

Layered Approach Continued

#blue Presentation Layer

Note

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

  • This layer handles external requests and includes controllers that validate inputs and pass them to the interactors in the Application layer. The more abstract layers of the program assume that data is already validated, allowing them to focus solely on their core logic.
  • Controllers must be as thin as possible, containing no logic beyond basic input validation and routing. Their role is to act as an intermediary between the application and external systems (e.g., FastAPI).

Important

  • Basic validation (e.g., type safety, required fields, input format) should be performed by controllers at this layer, while business rule validation (e.g., ensuring the email domain is allowed, verifying the uniqueness of username, or checking that a user meets the required age) belongs to the Domain layer.
  • Domain validation often involves relationships between fields, such as ensuring that a discount applies only within a specific date range or a promotion code is valid for orders above a certain total.
  • Pydantic is unsuitable for the Domain layer. Its parsing and serialization features have no relevance there, and while it might appear helpful for validation, it lacks the capabilities to handle complex relationships that may arise in Domain validation.

#gray External Layer

Note

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

  • 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 they interact with the application only through the Presentation and Infrastructure layers.

Basic Dependency Graph
Figure 3: Basic Dependency Graph

Dependency Inversion

The dependency inversion technique enables reversing dependencies by introducing an interface between components, allowing the inner layer to communicate with the outer layer while adhering to the Dependency Rule.

Corrupted Dependency
Figure 4: Corrupted Dependency

In this example, the Application component depends directly on the Infrastructure component, violating the Dependency Rule. This creates "corrupted" dependencies, where changes in the Infrastructure layer can propagate to and unintentionally affect the Application layer.

Correct Dependency
Figure 5: Correct Dependency

In the correct design, the Application layer component depends on an abstraction (port), and the Infrastructure layer component implements the corresponding interface. This makes the Infrastructure component an adapter for the port, effectively turning it into a plugin for the Application layer. Such a design adheres to the Dependency Inversion Principle (DIP), minimizing the impact of infrastructure changes on the core business logic.

Dependency Injection

The idea behind Dependency Injection is that a component shouldn't create the dependencies it needs but rather receive them. From this definition, it's clear that one common way to implement DI is by passing dependencies as arguments to the __init__ method or functions.

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 6: Correct Dependency with DI

FastAPI provides a built-in DI mechanism called Depends, which tends to leak into different layers of the application. This creates tight coupling to FastAPI, violating the principles of Clean Architecture, where the web framework belongs to the outermost layer and should remain easily replaceable.

Refactoring the codebase to remove Depends when switching frameworks can be unnecessarily costly. It also has other limitations that are beyond the scope of this README. Personally, I prefer Dishka β€” a solution that avoids these issues and remains framework-agnostic.

Project

Dependency Graphs

Application Controller - Interactor

Application Controller - Interactor
Figure 7: Application Controller - Interactor

Application Interactor

Application Interactor
Figure 8: Application Interactor

Application Interactor - Adapter

Application Interactor - Adapter
Figure 9: Application Interactor - Adapter

Domain - Adapter

Domain - Adapter
Figure 10: Domain - Adapter

An infrastructure "interactor" may be required as a temporary solution in cases where a separate context exists but isn't physically separated into a distinct domain (e.g., not implemented as a standalone module within a monolithic application). In such cases, the "interactor" operates as an application-level interactor but resides in the infrastructure layer.

In this application, such "interactors" include those managing user accounts, such as registration, login, and logout.

Infrastructure Controller - Interactor

Infrastructure Controller - Interactor
Figure 11: Infrastructure Controller - Interactor

Infrastructure Interactor

Infrastructure Interactor
Figure 12: Infrastructure Interactor

Identity Provider (IdP) abstracts authentication details, linking the main business context with the authentication context. In this example, the authentication context is not physically separated, making it an infrastructure detail. However, it can potentially evolve into a separate domain.

Identity Provider

Identity Provider
Figure 13: Identity Provider

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
        β”‚   β”œβ”€β”€ common/                      # common layer objects
        β”‚   β”‚   β”œβ”€β”€ authorization/...        # authorization logic
        β”‚   β”‚   └── ...                      # ports, exceptions, etc.
        β”‚   β”œβ”€β”€ admin_create_user.py         # interactor
        β”‚   └── ...                          # other interactors
        β”œβ”€β”€ domain/                          # domain layer
        β”‚   β”œβ”€β”€ base/...                     # base declarations
        β”‚   └── user/...                     # user domain objects
        β”œβ”€β”€ infrastructure/...               # infrastructure layer
        β”‚   β”œβ”€β”€ session_context/...          # session-based auth context
        β”‚   β”‚   β”œβ”€β”€ common/...               # common context objects
        β”‚   β”‚   β”œβ”€β”€ log_in.py                # interactor
        β”‚   β”‚   └── ...                      # other interactors
        β”‚   └── ...                          # adapters, exceptions, etc.
        β”œβ”€β”€ presentation/...                 # presentation layer
        β”‚   β”œβ”€β”€ common/...                   # common layer objects
        β”‚   └── http_controllers/            # controllers (http)
        β”‚       β”œβ”€β”€ user_change_password.py  # controller
        β”‚       └── ...                      # other controllers
        └── setup/
            β”œβ”€β”€ app_factory.py               # app builder
            β”œβ”€β”€ config/...                   # app settings
            └── ioc/...                      # dependency injection setup

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 14: Handlers

General

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

Account (/api/v1/account)

  • /signup: Open to everyone.
    • Registers a new user with validation and uniqueness checks.
    • Passwords are peppered, salted, and stored as hashes.
    • A logged-in user cannot sign up until the session expires or is terminated.
  • /login: Open to everyone.
    • Authenticates registered 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. 1
  • /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.
    • Only super admins can create new admins.
  • / (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.
    • Also deletes the user's sessions.
    • Only super admins can inactivate other admins.
  • /reactivate: Open to admins.
    • Restores a previously soft-deleted user.
    • Only super admins can reactivate other admins.
  • /grant: Open to super admins.
    • Grants admin rights to a specified user.
  • /revoke: Open to super admins.
    • Revokes admin rights from a specified user.
  • /change_password: Open to authenticated users.
    • Changes the user's password.
    • The current user can change their own password.
    • Admins can change passwords of subordinate users.

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 15: Configuration flow (.toml to .env)

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

Configuration flow (app)
Figure 16: Configuration flow (app)

Local Development

Important

The following make commands require Python >= 3.11 installed on your system. Feel free to take a look at Makefile, it contains many more useful commands.

  1. Set up development environment
# sudo apt update
# sudo apt install pipx
# pipx install uv
uv venv
source .venv/bin/activate
# .venv\Scripts\activate  # Windows
# uv python install 3.12  # if you don't have it
uv pip install -e '.[test,dev]'
  1. Configure project
  • Edit config.toml for primary configuration.

Warning

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

  • Generate .env file in one of the ways:
    1. Safe (as long as config.toml is correct)

      Set POSTGRES_HOST variable in config.toml to localhost, then:

      make dotenv
    2. Convenient:

      make dotenv-local

      Automatically sets correct configuration

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

    3. Manual (use with caution):

      Rename .env.example to .env and verify all variables.

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

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

    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.

  1. Shutdown
  • To stop the database container, use:

    make down
    # or
    # docker compose down
  • To permanently delete the database along with the applied migrations, run:

    make down-total
    # or
    # docker compose down -v

Docker Deployment

Important

The following make commands require Python >= 3.11 installed on your system. Feel free to take a look at Makefile, it contains many more useful commands.

  1. Configure project
  • Edit config.toml for primary configuration.

Warning

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

  • Generate .env file in one of the ways:
    1. Safe (as long as config.toml is correct)

      Set POSTGRES_HOST variable in config.toml to the name of the Docker service from docker-compose.yaml, then:

      make dotenv
    2. Convenient:

      make dotenv-docker

      Automatically sets correct configuration

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

    3. Manual (use with caution):

      Rename .env.example to .env and verify all variables.

  1. Launch
  • Choose one of the following commands:

    • make up
      # to run in detached mode
      # or
      # docker compose up --build -d
    • make up-echo
      # to run in non-detached mode
      # or
      # docker compose up --build
  1. Shutdown
  • To stop the containers, use:

    make down
    # or
    # docker compose down
  • To completely remove the containers and permanently delete the database, run:

    make down-total
    # or
    # docker compose down -v

Useful Resources

Layered Architecture

Domain-Driven Design

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 and βš—οΈ Reagento (adaptix/dishka) Telegram chat for their insightful discussions and shared knowledge.

Todo

  • set up CI
  • increase test coverage
  • explain the code

Footnotes

  1. Token and session share the same expiry time, avoiding database reads if the token is expired. ↩