Library / SDK
zhanymkanov/fastapi-best-practices avatar
zhanymkanov/fastapi-best-practices

zhanymkanov/fastapi-best-practices: an opinionated structure and convention guide for FastAPI monoliths

FastAPI Best Practices and Conventions we used at our startup

18,130 stars1,323 forksUnknownLicense varies

At a glance

What is it?
A repository of conventions drawn from several years of production FastAPI work, centred on a domain-first project layout and on async routes that do not block the event loop. The advice is opinionated, the repository ships no code to install, and it is aimed at teams outgrowing tutorial-style layouts.
Who is it for?
Adopt this if you are building a FastAPI monolith with several domains and your current layout is organised by file type, because the src/<domain>/ package layout is the part of the repository with the most concrete payoff. Do not adopt it if you want a runnable template: the repository is documentation only, with no package, no setup.py and no install command, so there is nothing to pip install.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 34 days ago.
What is it written in?
GitHub does not report a main language for this repository.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the fastapi-best-practices repository actually is

This is not a library. The README opens by calling itself an "Opinionated list of best practices and conventions we use at our startups", and the repository tree confirms it: the top level holds AGENTS.md, README.md, README_ZH.md and an images directory. There is no src package to import, no pyproject.toml, no CLI. You read it and you copy the patterns into your own project.

The intended audience is narrow and worth stating plainly. The project structure section explains that file-type organisation (crud, routers, models) works for microservices and smaller projects but "didn't scale well for our monolith with many domains and modules". So the guide is written for teams whose FastAPI app has grown past a handful of routers, and who are now deciding where business logic lives. If you are writing a single-endpoint service, most of the structure advice is overhead you do not need.

The framing is experience-based rather than prescriptive: after several years of production systems, the authors say they made good and bad decisions that affected developer experience. That is the honest register of the document. It does not claim to be the only correct answer, and the structure section says outright that many layouts exist.

The src/<domain>/ layout and how modules talk to each other

The core mechanism is a package per domain under src, each with a fixed set of files. Every domain directory carries router.py for endpoints, schemas.py for Pydantic models, models.py for database models, service.py for business logic, dependencies.py for router dependencies, constants.py for error codes, config.py for environment variables, utils.py for non-business helpers, and exceptions.py for domain-specific errors such as PostNotFound.

Above the domains sit shared modules: src/config.py for global configuration, src/models.py for global models, src/exceptions.py, src/pagination.py, src/database.py for connection handling, and src/main.py as the root that initialises the FastAPI app. Tests mirror the domain split under tests/auth, tests/aws and tests/posts, and requirements are split into base.txt, dev.txt and prod.txt.

The rule that makes this work at scale is the import convention. When one package needs something from another, the README says to import it with an explicit module name rather than a bare symbol:

python
from src.auth import constants as auth_constants
from src.notifications import service as notification_service
from src.posts.constants import ErrorCode as PostsErrorCode

The effect is that every cross-domain call is visible at the call site as a qualified name. A reviewer scanning a diff can see that posts now depends on notifications without opening the import block. The trade-off is verbosity: long qualified names everywhere, and no convenient re-export module. The README does not discuss circular imports between domains, which is the failure mode this convention is most likely to surface late.

Async routes, blocking calls, and the threadpool boundary

The async section is the most technical part of the document, and it rests on one distinction. FastAPI runs sync routes in a threadpool, so blocking I/O inside a def route does not stop the event loop. An async def route is awaited directly, and the framework trusts you to perform only non-blocking operations. Break that trust and the loop stalls.

The README illustrates this with three ping endpoints. The terrible one sleeps with time.sleep inside an async route and blocks the whole process. The good one uses time.sleep inside a sync route, so it runs in a separate thread. The perfect one awaits asyncio.sleep, which is non-blocking:

python
@router.get("/terrible-ping")
async def terrible_ping():
    time.sleep(10) # I/O blocking operation for 10 seconds, the whole process will be blocked

    return {"pong": True}

@router.get("/good-ping")
def good_ping():
    time.sleep(10) # I/O blocking operation for 10 seconds, but in a separate thread for the whole `good_ping` route

    return {"pong": True}

@router.get("/perfect-ping")
async def perfect_ping():
    await asyncio.sleep(10) # non-blocking I/O operation

    return {"pong": True}

The practical consequence is that mixing sync and async SDKs carelessly is the most common way to get this wrong. The guide's position is that if you must use a sync SDK, you run it in a thread pool rather than calling it from an async route. Note that the README does not provide a complete worked example of that thread pool wrapper in the sections shown, so you will need to consult the FastAPI documentation it links for the exact call. The CPU-bound case is separated from the I/O case, which is the right split: no thread pool fixes a CPU-bound route, and the event loop is the wrong place for that work either way.

Applying the structure to a first domain

There is nothing to install. The repository has no package metadata and no release artifacts, and the README gives no pip command. You consume it by reading and copying. The closest thing to an onboarding step is the pointer in the README: working with an AI agent, see AGENTS.md for the same rules in a machine-readable format with a version matrix, Do/Don't blocks and an anti-patterns checklist.

The first real use is to create one domain package and move an existing router into it. The README prints the full layout; the domain directories under src are auth, aws and posts, and each contains the file set shown in that tree, for example:

text
src
├── auth
│   ├── router.py
│   ├── schemas.py  # pydantic models
│   ├── models.py  # db models
│   ├── dependencies.py
│   ├── config.py  # local configs
│   ├── constants.py
│   ├── exceptions.py
│   ├── service.py
│   └── utils.py

Then register the router from src/main.py the way you already do. The README's structure section does not print the main.py wiring, so keep your existing include_router call and only change the import path to the new module. What you should see afterwards is a diff where every changed import is a src.<domain> path, and no business logic left in the old file-type directory.

The second step the README implies is splitting requirements. It lists requirements/base.txt, requirements/dev.txt and requirements/prod.txt as part of the layout, so if you are still on one requirements.txt, that split is the next mechanical change. Nothing here tells you which packages belong in which file, and the repository does not include the file contents, so the grouping is yours to decide.

Where the advice is thin or contested

The guide is opinionated, and a few positions deserve pushback before you adopt them wholesale. The structure section recommends a layout inspired by Netflix's Dispatch with minor modifications, and it is honest that this is a monolith answer to a monolith problem. Applied to a three-service system, the per-domain file set multiplies boilerplate: nine files per domain, several of which will stay nearly empty for a long time.

The async guidance is correct but incomplete on tooling. Blocking calls inside async routes are silent failures, and the README explains the mechanism without naming a detector. Nothing in the repository checks your code for a blocking call in an async def; that is a linting or runtime-instrumentation job you bring yourself.

The Pydantic and SQLAlchemy sections assume you are on current major versions of those libraries, and the README does not state version floors in the body text. The AGENTS.md file is described as carrying a version matrix, which suggests the compatibility information lives there rather than in the README. If your project is pinned to an older Pydantic, treat the schema advice as a target rather than a description of your codebase.

Finally, the repository is documentation with no test suite of its own. The code samples are illustrative, not executed examples you can run to confirm behaviour.

How it compares with a runnable FastAPI template

The obvious alternative is a scaffold generator or a full-stack FastAPI template that produces a working application with Docker, migrations and tests already wired. The difference in approach is the whole point: a template answers "what do I run" and this repository answers "how do I decide". A template gives you a repository you can start, with an opinion baked into code you can execute and modify. This guide gives you prose rules, one directory listing, and short code samples, and leaves the wiring to you.

That makes the two complementary rather than competing. If you want a running app in ten minutes, a template is the right tool and this repository will feel like reading a style guide before you have anything to style. If you already have an app and are arguing about where service.py belongs, a template will not settle the argument because it will simply impose its own answer, often the file-type layout the README argues against. The strongest reason to read this instead of adopting a template is that it explains the reasoning behind each choice, including why the file-type organisation stops scaling, which a generated repository cannot do.

A second alternative is the official FastAPI documentation itself. It covers async semantics and dependency injection in more depth than this repository does. Where the guide adds value is the connective tissue: which of those features to reach for in a multi-domain monolith, and which conventions to enforce in review.

Maintenance, licence and the cost of following along

The repository is not archived, and the last push was on 2026-08-27, so it has been touched recently. That matters less than it would for a library, because there is no dependency to upgrade and no breaking change to absorb. Following the guide costs you a refactor, not a version bump. The ongoing cost is review discipline: the import convention and the domain file set only hold if reviewers enforce them, and nothing in the repository automates that.

The licence is not stated. The repository metadata gives no licence identifier, and the README does not mention one. If you intend to copy the directory listing or code samples into your own project, confirm the licence from the repository page first; without a stated licence, the default position under copyright is that no reuse rights are granted, though I am not giving legal advice and you should take your own view.

The README_ZH.md file is a Simplified Chinese translation of the same content, and AGENTS.md is described as the same rules in a terse, machine-readable format. Both are maintained alongside the English README, which suggests the authors intend the guide to stay current rather than be a one-off post. There are no releases, so there is no changelog to watch; the commit history on master is the only record of what changed.

Editorial conclusion

Adopt this if you are building a FastAPI monolith with several domains and your current layout is organised by file type, because the src/<domain>/ package layout is the part of the repository with the most concrete payoff. Do not adopt it if you want a runnable template: the repository is documentation only, with no package, no setup.py and no install command, so there is nothing to pip install. Before committing, check the AGENTS.md file for the version matrix it promises, and confirm that the Pydantic, Alembic and SQLAlchemy versions you already run are covered there, since the README pages assume current releases without stating a floor.

Frequently asked questions

What is the best structure for a FastAPI project according to zhanymkanov/fastapi-best-practices?

The guide recommends a domain-first layout with each package under src holding its own router.py, schemas.py, models.py, service.py, dependencies.py, constants.py, config.py, utils.py and exceptions.py, with shared config, models, exceptions, pagination and database modules at the src level. It argues this scales better than organising by file type for a monolith with many domains. The layout is credited as inspired by Netflix's Dispatch with minor modifications.

What are the downsides of using FastAPI that this guide addresses?

The guide focuses on one specific downside: an async route that performs blocking I/O stalls the event loop, because FastAPI awaits async routes directly instead of offloading them to a threadpool. Its answer is to use sync routes for blocking work, or to run sync SDK calls in a thread pool. The repository does not survey FastAPI's limitations more broadly.

How do I install zhanymkanov/fastapi-best-practices?

You do not install it. The repository contains documentation only, with no package metadata and no install command in the README, so there is nothing to add to requirements. You read it and apply the conventions to your own FastAPI project.

Does zhanymkanov/fastapi-best-practices cover database migrations?

Yes. The contents list includes a Migrations section naming Alembic, and the project layout shows an alembic directory at the top level alongside alembic.ini. The guide also has sections on setting database key naming conventions and on a SQL-first, Pydantic-second approach.

Is there a machine-readable version of the FastAPI best practices rules?

The README points to AGENTS.md, describing it as the same rules in a terse, machine-readable format with a version matrix, Do/Don't blocks and an anti-patterns checklist. It is listed at the top level of the repository alongside README.md and README_ZH.md.

Official sources

  1. Issues
  2. README
  3. zhanymkanov/fastapi-best-practices on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/zhanymkanov-fastapi-best-practices.svg)](https://hysenlabs.com/projects/zhanymkanov-fastapi-best-practices)