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

FastAPI Best Practices: per-domain folders, and the async route that stalls the loop

FastAPI Best Practices and Conventions we used at our startup

18,323 stars1,331 forksUnknownLicense varies

At a glance

What is it?
The repository is prose, not a library. It is one startup's opinionated FastAPI conventions, from a Netflix Dispatch inspired domain layout to the threadpool behavior that decides whether a blocking SDK call occupies one worker thread or the whole process. Useful as a review checklist to argue with; useless as something you can install or pin.
Who is it for?
Adopt this layout when you have a monolith with several domains and one author to keep conventions consistent, and use the async rules as a code review gate rather than a style preference. Do not adopt it for a handful of endpoints, and do not expect a library: there are no releases, no version pin, and no reference code.
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 79 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 November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • zhanymkanov/fastapi-best-practices18,323 ★ · +226
Star history
Repository2026-09-212026-11-13
zhanymkanov/fastapi-best-practices18.1k18.3k

226 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Grouping by file type stopped scaling, so the layout became one folder per domain

The stated reason for the tree is a failure. Tutorials that sort code into crud, routers and models work well for microservices or smaller projects, the author writes, but that grouping did not scale for a monolith with many domains and modules. The replacement is a layout inspired by Netflix's Dispatch with minor modifications, where alembic/ sits at the root and everything else lives under src/. That src/ directory is the highest level of the app and holds the common models, configs and constants, and src/main.py is the root of the project where the FastAPI app is initialized. The cost is symmetry with a price tag. A two-endpoint service now carries the same shape as a busy domain, so files appear that have nothing in them yet, and the reader has to learn that a blank models.py in a new package is a slot waiting for content, not a bug in the scaffold.

Every domain package repeats the same nine filenames, each with a fixed job

The nine slots are spelled out one by one: router.py is the core of each module with all the endpoints, schemas.py holds Pydantic models, models.py the database models, service.py the module specific business logic, dependencies.py the router dependencies, constants.py the module constants and error codes, config.py local configuration such as environment variables, utils.py non-business functions such as response normalization and data enrichment, and exceptions.py module specific exceptions such as PostNotFound and InvalidUserData. Nothing forces every slot to be filled. In the same tree the aws package drops router.py, models.py, service.py and dependencies.py, and adds client.py for external service communication, while posts stops after three files. The consequence for a reader is a global search for a helper that returns one hit per domain, and a file that opens empty.

Cross-package imports name the module and rename the symbol

When one package needs services, dependencies or constants from another, the convention is an explicit module import, and the examples rename on the way in:

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  # in case we have Standard ErrorCode in constants module of each package

The third line explains the policy. Every package's constants module is expected to carry its own ErrorCode, so two unaliased imports of that name in one file would mean two different things. Renaming to PostsErrorCode and prefixing the other two imports with their source module keeps the origin visible at every call site. The price is verbosity: a file touching three domains reads with three prefixed names, and a reviewer has to check the alias to know which package a symbol came from. Nothing in the tree explains how to reverse this if a project prefers flat imports.

sync routes are offloaded to a threadpool, async routes are not

The mechanical claim underneath the whole document is stated in three routes:

python
import asyncio
import time

from fastapi import APIRouter

router = APIRouter()

@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}

Both sleeps take ten seconds, yet they are not the same. FastAPI runs sync routes in a threadpool, so blocking I/O inside a sync route cannot stop the event loop. A route defined as async is called via await instead, and FastAPI trusts you to perform only non-blocking I/O. The same ten second sleep therefore freezes the process in one case and occupies a worker thread in the other, which means a sync SDK called from a tidy async handler degrades every endpoint in the process, not just its own.

One blocking call inside an async route stops the event loop and every queued task

The walkthrough of GET /terrible-ping names the damage in order. The server receives the request and starts handling it, then the server's event loop and all queued tasks wait until time.sleep finishes, because only a sync route is offloaded to a threadpool and this one is async. The server will not accept any new requests while it waits, and only after responding does it resume accepting them. GET /good-ping takes the other path: the entire route is sent to the threadpool, a worker thread runs the function, and the event loop keeps processing other tasks such as accepting new requests and calling the database while the worker waits on time.sleep independently. The third route replaces the sleep with await asyncio.sleep(10), which yields control instead of holding it. That difference is the reason the rules exist, and the document splits the whole subject into I/O intensive tasks and CPU intensive tasks because the two need different treatment.

The rest of the guidance arrives as headings, and the titles carry the argument

Under Dependencies the document names Beyond Dependency Injection, Chain Dependencies, Decouple and Reuse dependencies with the warning that dependency calls are cached, and Prefer async dependencies. Under Pydantic it pushes Excessively use Pydantic, a Custom Base Model, and Decouple Pydantic BaseSettings. The Miscellaneous block covers Follow the REST, FastAPI response serialization, the rule that a sync SDK must run in a thread pool, BackgroundTasks versus a real task queue, ValueErrors that might surface as a Pydantic ValidationError, DB key naming conventions, Migrations with Alembic, DB naming conventions, SQL-first and Pydantic-second, setting the tests client async from day 0, and Use ruff. Reuse and caching share one heading, so the advice is not simply call the same dependency everywhere. A Bonus Section closes the contents. A team adopting any of it has to translate the wording into its own code.

The repository is three text files and an images folder, with no releases

Everything above lives in a repository that ships nothing to install. The top level holds AGENTS.md, README.md, README_ZH.md and an images directory, the default branch is master, and there are no GitHub releases. The language and license are not declared in the repository metadata either. The practical effect is that there is no tag to pin, no changelog to diff, and no version string to write into a dependency file. When the author changes a convention, the only signal a follower receives is a new commit on master, last pushed on 27 August 2026. Teams that want a stable house style have to vendor the text into their own repository and own the drift from there. The document describes itself as an opinionated list of conventions the author uses at their startups, not as framework guidance.

AGENTS.md and README_ZH.md repeat the same advice in two more shapes

Two of the three files restate the rules, which is convenient until they drift apart. README_ZH.md is the Simplified Chinese version of the README, linked from its first lines, and AGENTS.md is offered as the same rules in a terse, machine-readable format with a version matrix, Do and Don't blocks, and an anti-patterns checklist. That is three copies of one opinionated list, and the repository states no rule about which copy wins when they disagree. The version matrix matters most for a reader deciding whether to adopt a rule, because it implies the conventions are tied to specific FastAPI versions. Since the repository publishes no releases, checking that matrix against the release you actually run is left to you.

Editorial conclusion

Adopt this layout when you have a monolith with several domains and one author to keep conventions consistent, and use the async rules as a code review gate rather than a style preference. Do not adopt it for a handful of endpoints, and do not expect a library: there are no releases, no version pin, and no reference code. Before you commit, check which FastAPI release the rules were written against, using the version matrix that AGENTS.md carries.

Frequently asked questions

Is Netflix using FastAPI?

The folder layout in this repository is described as inspired by Netflix's Dispatch with minor modifications. The document makes no claim that Netflix runs FastAPI, and Dispatch is a separate project linked from the same sentence.

What are the downsides of using FastAPI?

FastAPI accepts both async and sync routes, and the two behave differently. A sync route is sent to a threadpool, so blocking I/O inside it does not stop the event loop, while an async route is called via await and FastAPI trusts you to keep I/O non-blocking, so a blocking call there holds the event loop and all queued tasks until it finishes.

What is the best structure for a FastAPI project?

This repository argues against sorting code by file type such as crud, routers and models, which works for microservices or smaller projects but did not scale for a monolith with many domains. Its answer is one folder per domain under src/, each with router.py, schemas.py, models.py, service.py, dependencies.py, constants.py, config.py, utils.py and exceptions.py.

Is FastAPI harder than Flask?

This repository draws no comparison with Flask, since it covers conventions inside a FastAPI codebase. What it does describe is that FastAPI does not restrict you to async routes, and that a sync route is offloaded to a threadpool while an async route is awaited and trusted to stay non-blocking.

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)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/zhanymkanov-fastapi-best-practices.svg)](https://hysenlabs.com/projects/zhanymkanov-fastapi-best-practices)

Was this useful?