fastapi-clean-example: Clean Architecture you can read in an afternoon
Practical Clean Architecture backend example built with FastAPI. No stateful globals (DI), low coupling (DIP), tactical DDD, CQRS, proper UoW usage. REST API, per-route error handling, session-based auth, contextual RBAC. Bundled with extensive docs and modern tooling
At a glance
- What is it?
- A FastAPI backend example organised around dependency injection, tactical DDD, CQRS and the unit of work, with every dependency pinned to an exact version and a Makefile that drives Docker, migrations and tests. The architecture documentation currently lives on a legacy branch while a refactor is in progress.
- Who is it for?
- fastapi-clean-example suits a Python team that wants a worked reference for Clean Architecture on FastAPI, with the boundaries enforced by tooling rather than by convention, and with a project that is current enough to run against Python 3.13 and current FastAPI. It does not suit a team looking for a finished framework, since the README says the refactor is in progress and the template release is still a TODO.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 16 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the example is trying to demonstrate
The repository describes itself as a practical Clean Architecture backend example built with FastAPI, and it lists the properties it is demonstrating in one long line. No stateful globals, with dependency injection doing the work. Low coupling, through the dependency inversion principle. Tactical DDD. CQRS. Proper unit of work usage. A REST API with per-route error handling, session-based authentication and contextual RBAC. And it says it is bundled with extensive docs and modern tooling.
That is an unusually concrete claim list for an example repository, and it is useful as a checklist when you read the code. The interesting question is not whether the folders are named correctly but whether the dependencies actually point in the right direction, and this project takes a mechanical approach to that.
Two things about the repository's current state need saying up front. The README opens with Stay tuned, refactor in progress, and points to the legacy-2025 branch for the architecture documentation. So the code on master and the prose describing it are not in step, and the documentation you want is on a branch named after last year. The TODO list is equally honest: write tests is ticked, explain code and patterns in new README is not, and make template project is not.
That third item tells you what the author intends this to become. A template project is a different product from an example, and it is the natural next step, but it is not there yet.
Prerequisites are uv and a pre-commit installer
The setup is three commands, and the toolchain is worth naming because it says something about the project's priorities.
uv sync
source .venv/bin/activate
prek install --overwrite --hook-type pre-commit --hook-type pre-pushuv is the package and environment manager, and uv.lock sits at the repository root next to pyproject.toml. prek is the hook runner, and it is installed with the overwrite flag for both the pre-commit and pre-push hook types, so the checks run before a commit and before a push.
The third command is the one that tells you the most about how this repository is maintained. A pre-push hook is not the default choice. It means the maintainer expects contributors to run the linting, type checking and tests locally before anything leaves their machine, which in a repository with an import-linter contract and a strict dependency set is the difference between a clean pull request and a broken one.
The top level also carries a .pre-commit-config.yaml, a .github/ directory, a Dockerfile, a Makefile, alembic.ini, env.example, four compose files, docker-entrypoint.sh, scripts/, src/, tests/ and uv.lock. The scripts/ directory is where the Makefile's logic lives, and the presence of scripts/makefile/ subfolder with one script per concern is the shape you would expect from a project that treats the Makefile as an API.
Docker, local, and the difference between check and test
There are two ways to start the application, and the Docker path is the short one.
make updLocally there are more steps, and the order matters because the database schema has to exist before the application talks to it.
make upd-local
alembic upgrade head
uvicorn app.main.run:make_app --factory --host 0.0.0.0 --port 8000 --reload
# or `src/app/main/run.py` in IDEThe uvicorn invocation is unusual and deliberate. The app is passed as a factory with the --factory flag, pointing at app.main.run:make_app, which is what lets the project keep its dependency injection container and configuration assembled outside of a module level global. The comment offers the alternative for people who prefer to run src/app/main/run.py from an IDE.
Stopping is `make down`, and the Makefile is documented as having more commands.
The test commands are split, and the split is the interesting part. `make check` runs the light paths, and `make test-docker` runs all the paths.
make checkmake test-dockerMigrations are generated through the Makefile too, with the message passed as a variable.
make migration msg=<msg>The Makefile itself documents the configurable variables, and they show how the test infrastructure is designed. INFRA_SERVICES lists the long-running infrastructure such as the database, a broker or a cache, INFRA_INIT_SERVICES the one-shot services that prepare them, MIGRATION_DB_SERVICE and MIGRATION_DB_PORT the transactional database used by alembic, STAIRWAY_TEST the path to the stairway test, and PYTEST_WORKERS the number of parallel pytest processes, each of which gets its own test database, with a default of 4.
Full API access needs a role set by hand
The README describes the path to exercising everything, and it contains a step that tells you about the auth model.
Full API access works like this. You create a user through sign up, you set its role to super_admin manually in the database, and you then log in as that super admin.
That manual step is not an oversight in the documentation, it is a deliberate position on how a new account gets administrative rights. There is no self-service role assignment and no admin UI in the flow described here, so the first privileged account has to be created with a direct database edit. The README says it plainly: set its role to super_admin manually in DB.
The two related mechanisms are worth naming. Authentication is session-based rather than token-based for the browser flow, and the dependency list shows pyjwt with the crypto extra alongside bcrypt, with the comment that starlette ships with fastapi and that the project writes its own auth cookie middleware on it. That matches the repository's whole approach: take the framework as given, write the interesting layer yourself.
RBAC is described as contextual, which is the more interesting half of the claim. Contextual means the permission question is asked with the request context in hand, so the same role can behave differently depending on what the request is about, rather than a flat list of roles checked against a route.
Every dependency is pinned to an exact version
The pyproject.toml is where this project states its standards most clearly, and the pinning policy is absolute. Every runtime dependency carries an exact version, with no ranges.
The runtime list includes alembic 1.20.0, bcrypt 5.0.0, dishka 1.10.1, fastapi 0.142.2, fastapi-error-map 1.0.0, psycopg with the binary extra at 3.3.6, pydantic 2.13.5, pydantic-settings 2.15.0, pyjwt with crypto at 2.15.1, sqlalchemy 2.0.54, starlette 1.7.0, uuid-utils 1.0.0 and uvicorn 0.54.0. The interpreter is pinned just as hard, with requires-python set to ==3.13.*.
Two comments in the dependency list explain choices that would otherwise look arbitrary. pydantic ships with fastapi, and the project says it builds its settings and request schemas on it. starlette ships with fastapi too, and the project writes its auth cookie middleware on it. Both are statements about where the interesting work belongs: use the library for the parts that are plumbing, and write the part that is about authentication or configuration yourself.
dishka is the dependency injection container, and the Makefile points at a script that renders a dependency graph from it, scripts/dishka/render_di_graph.py. Being able to export the container as a graph is a small thing that makes the architecture inspectable, which is exactly the point of an example repository.
The development group is longer and reveals the toolchain: ruff, mypy, pytest with asyncio, coverage and xdist, deptry for unused dependencies, import-linter for the architecture contract, pip-audit for known vulnerabilities, slotscheck, tombi for TOML formatting, line-profiler, and prek for the hooks. The coverage configuration omits src/app/main, the health endpoints and the alembic directory, and runs with thread concurrency and sysmon as the core.
The containers assume a local database on loopback
The compose file is small and opinionated, and it is where the local development assumptions become visible.
The app service builds from the repository Dockerfile with an ENVIRONMENT build arg of dev, mounts the working tree at /code, and publishes the port as 127.0.0.1:8000:8000, bound to loopback rather than to the host interface. It waits for the database to be healthy before starting, reads its environment from an env_file that defaults to.env, and runs the command start 8000.
The database service is postgres:18-alpine with a shared memory size of 128mb, its own healthcheck built on pg_isready with a two second interval and five retries, and a start period of one minute. The Postgres credentials come from the environment with the ${VAR:?} form, which makes the compose file fail fast if they are unset rather than starting with a default password.
The Dockerfile is built on the official uv image for Python 3.13 on a trixie slim base. It installs dependencies in two stages, with dev dependencies included in the dev build and excluded in the prod build, then creates a non-root runner user with a nologin shell, gives the code directory to that user, exposes port 8000 and runs docker-entrypoint.sh as the entrypoint.
There are two more compose files for the same project: docker-compose.migration.yml for running migrations against a database, docker-compose.test.yml for the test environment, and the main docker-compose.yml. That separation, with the migration service on its own host port so it does not collide with the running app, is what the Makefile variables MIGRATION_DB_SERVICE and MIGRATION_DB_PORT are there to configure.
Where this example stops being the right reference
The first case is a team that wants a framework. This is an example, and the README is explicit that making a template project is still an open TODO. You are getting a worked sample of how one author organises a FastAPI backend, not a maintained base with a versioning policy, an upgrade path or a support commitment.
The second case is a team that needs the architecture documented. That is the awkward part of the current state. The architecture documentation lives on the legacy-2025 branch, the master branch is mid-refactor, and the TODO for explaining the code and patterns in a new README is unticked. A reader who starts on master has the code and not the map.
The third case is anything that needs to run on a different Python. requires-python is ==3.13.*, the Dockerfile pins Python 3.13, and the dependency versions are the ones that were current for that interpreter. Adapting this to a different version is work that the pinning makes explicit rather than hides, which is a good property, but it is work.
For the case it does target, the value is specific. A developer who knows FastAPI but not Clean Architecture, or who has read Clean Architecture and never seen it applied, gets a codebase where the dependency inversion is real rather than decorative, where the import-linter and the rendered dependency graph can be checked rather than asserted, and where the tests, the migrations and the container setup are already wired together.
Editorial conclusion
fastapi-clean-example suits a Python team that wants a worked reference for Clean Architecture on FastAPI, with the boundaries enforced by tooling rather than by convention, and with a project that is current enough to run against Python 3.13 and current FastAPI. It does not suit a team looking for a finished framework, since the README says the refactor is in progress and the template release is still a TODO. Verify first that you are reading the master branch, because the architecture documentation is on the legacy-2025 branch, and expect to spend a day reading src/ before the structure stops being unfamiliar.
Frequently asked questions
How do I start the fastapi-clean-example project locally?
Run make upd-local, then alembic upgrade head, then uvicorn app.main.run:make_app --factory --host 0.0.0.0 --port 8000 --reload. The prerequisites are uv sync, activating .venv, and prek install --overwrite for the pre-commit and pre-push hooks.
Which Python version does fastapi-clean-example require?
The project pins requires-python to ==3.13.*, and the Dockerfile is built on the uv image for Python 3.13. Every runtime dependency is also pinned to an exact version.
How do I get full API access in fastapi-clean-example?
Create a user through sign up, set its role to super_admin manually in the database, and log in as that super admin. Elevation happens by hand, not through the app.
What is the difference between make check and make test-docker?
The README describes make check as testing the light paths and make test-docker as testing all the paths. The Makefile also exposes PYTEST_WORKERS, the number of parallel pytest processes, each of which gets its own test database, defaulting to 4.
Where is the architecture documentation for fastapi-clean-example?
The README says the refactor is in progress and points to the legacy-2025 branch for the architecture documentation. Explaining the code and patterns in a new README is still an open item in the project TODO list.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ivan-borovets-fastapi-clean-example)Was this useful?