[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-72728":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":10,"languages":10,"totalLinesOfCode":10,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":16,"stars7d":17,"stars30d":18,"stars90d":15,"forks30d":15,"starsTrendScore":19,"compositeScore":20,"rankGlobal":10,"rankLanguage":10,"license":10,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":23,"hasPages":21,"topics":24,"createdAt":10,"pushedAt":10,"updatedAt":27,"readmeContent":28,"aiSummary":29,"trendingCount":15,"starSnapshotCount":15,"syncStatus":30,"lastSyncTime":31,"discoverSource":32},72728,"fastapi-best-practices","zhanymkanov\u002Ffastapi-best-practices","zhanymkanov","FastAPI Best Practices and Conventions we used at our startup","",null,17475,1275,166,12,0,24,82,247,72,44.32,false,"master",true,[25,26],"best-practices","fastapi","2026-06-12 02:03:07","## FastAPI Best Practices \u003C!-- omit from toc -->\nOpinionated list of best practices and conventions we use at our startups.\n\nAfter several years of building production systems,\nwe've made both good and bad decisions that significantly impacted our developer experience.\nHere are some lessons worth sharing.\n\n> **Working with an AI agent?** See [AGENTS.md](.\u002FAGENTS.md) for the same rules in a\n> terse, machine-readable format with a version matrix, Do\u002FDon't blocks, and an\n> anti-patterns checklist.\n\n*[简体中文](.\u002FREADME_ZH.md)*\n\n## Contents  \u003C!-- omit from toc -->\n- [Project Structure](#project-structure)\n- [Async Routes](#async-routes)\n  - [I\u002FO Intensive Tasks](#io-intensive-tasks)\n  - [CPU Intensive Tasks](#cpu-intensive-tasks)\n- [Pydantic](#pydantic)\n  - [Excessively use Pydantic](#excessively-use-pydantic)\n  - [Custom Base Model](#custom-base-model)\n  - [Decouple Pydantic BaseSettings](#decouple-pydantic-basesettings)\n- [Dependencies](#dependencies)\n  - [Beyond Dependency Injection](#beyond-dependency-injection)\n  - [Chain Dependencies](#chain-dependencies)\n  - [Decouple \\& Reuse dependencies. Dependency calls are cached](#decouple--reuse-dependencies-dependency-calls-are-cached)\n  - [Prefer `async` dependencies](#prefer-async-dependencies)\n- [Miscellaneous](#miscellaneous)\n  - [Follow the REST](#follow-the-rest)\n  - [FastAPI response serialization](#fastapi-response-serialization)\n  - [If you must use sync SDK, then run it in a thread pool.](#if-you-must-use-sync-sdk-then-run-it-in-a-thread-pool)\n  - [BackgroundTasks vs a real task queue](#backgroundtasks-vs-a-real-task-queue)\n  - [ValueErrors might become Pydantic ValidationError](#valueerrors-might-become-pydantic-validationerror)\n  - [Docs](#docs)\n  - [Set DB keys naming conventions](#set-db-keys-naming-conventions)\n  - [Migrations. Alembic](#migrations-alembic)\n  - [Set DB naming conventions](#set-db-naming-conventions)\n  - [SQL-first. Pydantic-second](#sql-first-pydantic-second)\n  - [Set tests client async from day 0](#set-tests-client-async-from-day-0)\n  - [Use ruff](#use-ruff)\n- [Bonus Section](#bonus-section)\n\n## Project Structure\nThere are many ways to structure a project, but the best structure is one that is consistent, straightforward, and free of surprises.\n\nMany example projects and tutorials organize projects by file type (e.g., crud, routers, models), which works well for microservices or smaller projects. However, this approach didn't scale well for our monolith with many domains and modules.\n\nThe structure I found more scalable and evolvable is inspired by Netflix's [Dispatch](https:\u002F\u002Fgithub.com\u002FNetflix\u002Fdispatch), with some minor modifications.\n```\nfastapi-project\n├── alembic\u002F\n├── src\n│   ├── auth\n│   │   ├── router.py\n│   │   ├── schemas.py  # pydantic models\n│   │   ├── models.py  # db models\n│   │   ├── dependencies.py\n│   │   ├── config.py  # local configs\n│   │   ├── constants.py\n│   │   ├── exceptions.py\n│   │   ├── service.py\n│   │   └── utils.py\n│   ├── aws\n│   │   ├── client.py  # client model for external service communication\n│   │   ├── schemas.py\n│   │   ├── config.py\n│   │   ├── constants.py\n│   │   ├── exceptions.py\n│   │   └── utils.py\n│   ├── posts\n│   │   ├── router.py\n│   │   ├── schemas.py\n│   │   ├── models.py\n│   │   ├── dependencies.py\n│   │   ├── constants.py\n│   │   ├── exceptions.py\n│   │   ├── service.py\n│   │   └── utils.py\n│   ├── config.py  # global configs\n│   ├── models.py  # global models\n│   ├── exceptions.py  # global exceptions\n│   ├── pagination.py  # global module e.g. pagination\n│   ├── database.py  # db connection related stuff\n│   └── main.py\n├── tests\u002F\n│   ├── auth\n│   ├── aws\n│   └── posts\n├── templates\u002F\n│   └── index.html\n├── requirements\n│   ├── base.txt\n│   ├── dev.txt\n│   └── prod.txt\n├── .env\n├── .gitignore\n├── logging.ini\n└── alembic.ini\n```\n1. Store all domain directories inside `src` folder\n   1. `src\u002F` - highest level of an app, contains common models, configs, and constants, etc.\n   2. `src\u002Fmain.py` - root of the project, which inits the FastAPI app\n2. Each package has its own router, schemas, models, etc.\n   1. `router.py` - is a core of each module with all the endpoints\n   2. `schemas.py` - for pydantic models\n   3. `models.py` - for db models\n   4. `service.py` - module specific business logic  \n   5. `dependencies.py` - router dependencies\n   6. `constants.py` - module specific constants and error codes\n   7. `config.py` - e.g. env vars\n   8. `utils.py` - non-business logic functions, e.g. response normalization, data enrichment, etc.\n   9. `exceptions.py` - module specific exceptions, e.g. `PostNotFound`, `InvalidUserData`\n3. When package requires services or dependencies or constants from other packages - import them with an explicit module name\n```python\nfrom src.auth import constants as auth_constants\nfrom src.notifications import service as notification_service\nfrom src.posts.constants import ErrorCode as PostsErrorCode  # in case we have Standard ErrorCode in constants module of each package\n```\n\n## Async Routes\nFastAPI is an async-first framework—it's designed to work with async I\u002FO operations, which is why it's so fast.\n\nHowever, FastAPI doesn't restrict you to only `async` routes; you can use `sync` routes too. This might confuse beginners into thinking they're the same, but they're not.\n\n### I\u002FO Intensive Tasks\nUnder the hood, FastAPI can [effectively handle](https:\u002F\u002Ffastapi.tiangolo.com\u002Fasync\u002F#path-operation-functions) both async and sync I\u002FO operations:\n- FastAPI runs `sync` routes in a [threadpool](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FThread_pool), so blocking I\u002FO operations won't stop the [event loop](https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fasyncio-eventloop.html) from executing other tasks.\n- If the route is defined as `async`, it's called via `await` and FastAPI trusts you to only perform non-blocking I\u002FO operations.\n\nThe caveat is that if you violate that trust and execute blocking operations within async routes, the event loop won't be able to run other tasks until the blocking operation completes.\n```python\nimport asyncio\nimport time\n\nfrom fastapi import APIRouter\n\n\nrouter = APIRouter()\n\n\n@router.get(\"\u002Fterrible-ping\")\nasync def terrible_ping():\n    time.sleep(10) # I\u002FO blocking operation for 10 seconds, the whole process will be blocked\n    \n    return {\"pong\": True}\n\n@router.get(\"\u002Fgood-ping\")\ndef good_ping():\n    time.sleep(10) # I\u002FO blocking operation for 10 seconds, but in a separate thread for the whole `good_ping` route\n\n    return {\"pong\": True}\n\n@router.get(\"\u002Fperfect-ping\")\nasync def perfect_ping():\n    await asyncio.sleep(10) # non-blocking I\u002FO operation\n\n    return {\"pong\": True}\n\n```\n**What happens when we call:**\n1. `GET \u002Fterrible-ping`\n   1. FastAPI server receives a request and starts handling it\n   2. Server's event loop and all queued tasks wait until `time.sleep()` finishes\n      1. Since the route is `async`, the server doesn't offload it to a threadpool—it blocks the entire event loop\n      2. Server won't accept any new requests while waiting\n   3. Server returns the response\n      1. Only after responding does the server resume accepting new requests\n2. `GET \u002Fgood-ping`\n   1. FastAPI server receives a request and starts handling it\n   2. FastAPI sends the entire `good_ping` route to the threadpool, where a worker thread runs the function\n   3. While `good_ping` executes, the event loop continues processing other tasks (e.g., accepting new requests, calling the database)\n      - The worker thread waits for `time.sleep` to finish, independently of the main thread\n      - The sync operation blocks only the worker thread, not the main event loop\n   4. When `good_ping` finishes, the server returns a response to the client\n3. `GET \u002Fperfect-ping`\n   1. FastAPI server receives a request and starts handling it\n   2. FastAPI awaits `asyncio.sleep(10)`\n   3. Event loop continues processing other tasks from the queue (e.g., accepting new requests, calling the database)\n   4. When `asyncio.sleep(10)` completes, the server finishes executing the route and returns a response to the client\n\n> [!WARNING]\n> Notes on the thread pool:\n> - Threads require more resources than coroutines, so they are not as cheap as async I\u002FO operations.\n> - Thread pool has a limited number of threads, i.e. you might run out of threads and your app will become slow. [Read more](https:\u002F\u002Fgithub.com\u002FKludex\u002Ffastapi-tips?tab=readme-ov-file#2-be-careful-with-non-async-functions) (external link)\n\n### CPU Intensive Tasks\nThe second caveat is that non-blocking awaitables and threadpool offloading are only beneficial for I\u002FO intensive tasks (e.g., file operations, database calls, external API requests).\n- Awaiting CPU-intensive tasks (e.g., heavy calculations, data processing, video transcoding) provides no benefit since the CPU must actively work to complete them. In contrast, I\u002FO operations are external—the server just waits for a response and can handle other tasks in the meantime.\n- Running CPU-intensive tasks in other threads is also ineffective due to the [GIL](https:\u002F\u002Frealpython.com\u002Fpython-gil\u002F). In short, the GIL allows only one thread to execute Python bytecode at a time, making threads ineffective for CPU-bound work.\n- To optimize CPU-intensive tasks, you should offload them to worker processes (e.g., using `multiprocessing` or a task queue like Celery).\n\n**Related StackOverflow questions of confused users**\n1. https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F62976648\u002Farchitecture-flask-vs-fastapi\u002F70309597#70309597\n   - Here you can also check [my answer](https:\u002F\u002Fstackoverflow.com\u002Fa\u002F70309597\u002F6927498)\n2. https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F65342833\u002Ffastapi-uploadfile-is-slow-compared-to-flask\n3. https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F71516140\u002Ffastapi-runs-api-calls-in-serial-instead-of-parallel-fashion\n\n## Pydantic\n### Excessively use Pydantic\nPydantic has a rich set of features to validate and transform data. \n\nIn addition to standard features like required and optional fields with default values,\nPydantic has built-in data processing tools like regex validation, enums, string manipulation, email validation, and more.\n```python\nfrom enum import StrEnum\nfrom pydantic import AnyUrl, BaseModel, EmailStr, Field\n\n\nclass MusicBand(StrEnum):\n   AEROSMITH = \"AEROSMITH\"\n   QUEEN = \"QUEEN\"\n   ACDC = \"AC\u002FDC\"\n\n\nclass UserBase(BaseModel):\n    first_name: str = Field(min_length=1, max_length=128)\n    username: str = Field(min_length=1, max_length=128, pattern=\"^[A-Za-z0-9-_]+$\")\n    email: EmailStr\n    age: int = Field(ge=18)  # required, must be greater or equal to 18\n    favorite_band: MusicBand | None = None  # only \"AEROSMITH\", \"QUEEN\", \"AC\u002FDC\" values are allowed to be inputted\n    website: AnyUrl | None = None\n```\n### Custom Base Model\nHaving a controllable global base model allows us to customize all the models within the app. For instance, we can enforce a standard datetime format or introduce a common method for all subclasses of the base model.\n```python\nfrom datetime import datetime\nfrom typing import Any\nfrom zoneinfo import ZoneInfo\n\nfrom fastapi.encoders import jsonable_encoder\nfrom pydantic import BaseModel, ConfigDict, field_serializer\n\n\nclass CustomModel(BaseModel):\n    model_config = ConfigDict(populate_by_name=True)\n\n    @field_serializer(\"*\", when_used=\"json\", check_fields=False)\n    def _serialize_datetimes(self, value: Any) -> Any:\n        if isinstance(value, datetime):\n            if value.tzinfo is None:\n                value = value.replace(tzinfo=ZoneInfo(\"UTC\"))\n            return value.strftime(\"%Y-%m-%dT%H:%M:%S%z\")\n        return value\n\n    def serializable_dict(self, **kwargs):\n        \"\"\"Return a dict which contains only serializable fields.\"\"\"\n        default_dict = self.model_dump()\n\n        return jsonable_encoder(default_dict)\n\n\n```\nIn the example above, we have decided to create a global base model that:\n- Serializes all datetime fields to a standard format with an explicit timezone\n- Provides a method to return a dict with only serializable fields\n### Decouple Pydantic BaseSettings\nBaseSettings is great for reading environment variables, but a single BaseSettings for the whole app gets messy. Split it across modules and domains.\n```python\n# src.auth.config\nfrom datetime import timedelta\n\nfrom pydantic_settings import BaseSettings\n\n\nclass AuthConfig(BaseSettings):\n    JWT_ALG: str\n    JWT_SECRET: str\n    JWT_EXP: int = 5  # minutes\n\n    REFRESH_TOKEN_KEY: str\n    REFRESH_TOKEN_EXP: timedelta = timedelta(days=30)\n\n    SECURE_COOKIES: bool = True\n\n\nauth_settings = AuthConfig()\n\n\n# src.config\nfrom pydantic import PostgresDsn, RedisDsn\nfrom pydantic_settings import BaseSettings\n\nfrom src.constants import Environment\n\n\nclass Config(BaseSettings):\n    DATABASE_URL: PostgresDsn\n    REDIS_URL: RedisDsn\n\n    SITE_DOMAIN: str = \"myapp.com\"\n\n    ENVIRONMENT: Environment = Environment.PRODUCTION\n\n    SENTRY_DSN: str | None = None\n\n    CORS_ORIGINS: list[str]\n    CORS_ORIGINS_REGEX: str | None = None\n    CORS_HEADERS: list[str]\n\n    APP_VERSION: str = \"1.0\"\n\n\nsettings = Config()\n\n```\n\n## Dependencies\n### Beyond Dependency Injection\nPydantic is a great schema validator, but for complex validations that require database or external service calls, it's not enough.\n\nFastAPI docs mostly present dependencies as DI for endpoints, but they're also great for request validation.\n\nDependencies can validate data against database constraints (e.g., checking if an email already exists, ensuring a user exists, etc.).\n```python\n# dependencies.py\nasync def valid_post_id(post_id: UUID4) -> dict[str, Any]:\n    post = await service.get_by_id(post_id)\n    if not post:\n        raise PostNotFound()\n\n    return post\n\n\n# router.py\n@router.get(\"\u002Fposts\u002F{post_id}\", response_model=PostResponse)\nasync def get_post_by_id(post: dict[str, Any] = Depends(valid_post_id)):\n    return post\n\n\n@router.put(\"\u002Fposts\u002F{post_id}\", response_model=PostResponse)\nasync def update_post(\n    update_data: PostUpdate,  \n    post: dict[str, Any] = Depends(valid_post_id), \n):\n    updated_post = await service.update(id=post[\"id\"], data=update_data)\n    return updated_post\n\n\n@router.get(\"\u002Fposts\u002F{post_id}\u002Freviews\", response_model=list[ReviewsResponse])\nasync def get_post_reviews(post: dict[str, Any] = Depends(valid_post_id)):\n    post_reviews = await reviews_service.get_by_post_id(post[\"id\"])\n    return post_reviews\n```\nIf we didn't put data validation in a dependency, we would have to validate that `post_id` exists\nin every endpoint and write the same tests for each of them. \n\n### Chain Dependencies\nDependencies can use other dependencies and avoid code repetition for similar logic.\n```python\n# dependencies.py\nfrom fastapi.security import OAuth2PasswordBearer\nimport jwt  # PyJWT\nfrom jwt.exceptions import InvalidTokenError\n\nasync def valid_post_id(post_id: UUID4) -> dict[str, Any]:\n    post = await service.get_by_id(post_id)\n    if not post:\n        raise PostNotFound()\n\n    return post\n\n\nasync def parse_jwt_data(\n    token: str = Depends(OAuth2PasswordBearer(tokenUrl=\"\u002Fauth\u002Ftoken\"))\n) -> dict[str, Any]:\n    try:\n        payload = jwt.decode(token, \"JWT_SECRET\", algorithms=[\"HS256\"])\n    except InvalidTokenError:\n        raise InvalidCredentials()\n\n    return {\"user_id\": payload[\"id\"]}\n\n\nasync def valid_owned_post(\n    post: dict[str, Any] = Depends(valid_post_id), \n    token_data: dict[str, Any] = Depends(parse_jwt_data),\n) -> dict[str, Any]:\n    if post[\"creator_id\"] != token_data[\"user_id\"]:\n        raise UserNotOwner()\n\n    return post\n\n# router.py\n@router.get(\"\u002Fusers\u002F{user_id}\u002Fposts\u002F{post_id}\", response_model=PostResponse)\nasync def get_user_post(post: dict[str, Any] = Depends(valid_owned_post)):\n    return post\n\n```\n### Decouple & Reuse dependencies. Dependency calls are cached\nDependencies can be reused multiple times, and they won't be recalculated - FastAPI caches dependency's result within a request's scope by default,\ni.e. if `valid_post_id` gets called multiple times in one route, it will be called only once.\n\nKnowing this, we can decouple dependencies onto multiple smaller functions that operate on a smaller domain and are easier to reuse in other routes.\nFor example, in the code below we are using `parse_jwt_data` three times:\n1. `valid_owned_post`\n2. `valid_active_creator`\n3. `get_user_post`,\n\nbut `parse_jwt_data` is called only once, in the very first call.\n\n```python\n# dependencies.py\nfrom fastapi import BackgroundTasks\nfrom fastapi.security import OAuth2PasswordBearer\nimport jwt  # PyJWT\nfrom jwt.exceptions import InvalidTokenError\n\nasync def valid_post_id(post_id: UUID4) -> Mapping:\n    post = await service.get_by_id(post_id)\n    if not post:\n        raise PostNotFound()\n\n    return post\n\n\nasync def parse_jwt_data(\n    token: str = Depends(OAuth2PasswordBearer(tokenUrl=\"\u002Fauth\u002Ftoken\"))\n) -> dict:\n    try:\n        payload = jwt.decode(token, \"JWT_SECRET\", algorithms=[\"HS256\"])\n    except InvalidTokenError:\n        raise InvalidCredentials()\n\n    return {\"user_id\": payload[\"id\"]}\n\n\nasync def valid_owned_post(\n    post: Mapping = Depends(valid_post_id), \n    token_data: dict = Depends(parse_jwt_data),\n) -> Mapping:\n    if post[\"creator_id\"] != token_data[\"user_id\"]:\n        raise UserNotOwner()\n\n    return post\n\n\nasync def valid_active_creator(\n    token_data: dict = Depends(parse_jwt_data),\n):\n    user = await users_service.get_by_id(token_data[\"user_id\"])\n    if not user[\"is_active\"]:\n        raise UserIsBanned()\n    \n    if not user[\"is_creator\"]:\n       raise UserNotCreator()\n    \n    return user\n        \n\n# router.py\n@router.get(\"\u002Fusers\u002F{user_id}\u002Fposts\u002F{post_id}\", response_model=PostResponse)\nasync def get_user_post(\n    worker: BackgroundTasks,\n    post: Mapping = Depends(valid_owned_post),\n    user: Mapping = Depends(valid_active_creator),\n):\n    \"\"\"Get post that belong the active user.\"\"\"\n    worker.add_task(notifications_service.send_email, user[\"id\"])\n    return post\n\n```\n\n### Prefer `async` dependencies\nFastAPI supports both `sync` and `async` dependencies. It's tempting to use `sync` when you don't need to await anything, but that's not the best choice.\n\nJust like routes, `sync` dependencies run in a threadpool. Threads have overhead that's unnecessary for small non-I\u002FO operations.\n\n[See more](https:\u002F\u002Fgithub.com\u002FKludex\u002Ffastapi-tips?tab=readme-ov-file#9-your-dependencies-may-be-running-on-threads) (external link)\n\n\n## Miscellaneous\n### Follow the REST\nDeveloping RESTful API makes it easier to reuse dependencies in routes like these:\n   1. `GET \u002Fcourses\u002F:course_id`\n   2. `GET \u002Fcourses\u002F:course_id\u002Fchapters\u002F:chapter_id\u002Flessons`\n   3. `GET \u002Fchapters\u002F:chapter_id`\n\nThe only caveat is having to use the same variable names in the path:\n- If you have two endpoints `GET \u002Fprofiles\u002F:profile_id` and `GET \u002Fcreators\u002F:creator_id`\nthat both validate whether the given `profile_id` exists,  but `GET \u002Fcreators\u002F:creator_id`\nalso checks if the profile is creator, then it's better to rename `creator_id` path variable to `profile_id` and chain those two dependencies.\n```python\n# src.profiles.dependencies\nasync def valid_profile_id(profile_id: UUID4) -> Mapping:\n    profile = await service.get_by_id(profile_id)\n    if not profile:\n        raise ProfileNotFound()\n\n    return profile\n\n# src.creators.dependencies\nasync def valid_creator_id(profile: Mapping = Depends(valid_profile_id)) -> Mapping:\n    if not profile[\"is_creator\"]:\n       raise ProfileNotCreator()\n\n    return profile\n\n# src.profiles.router.py\n@router.get(\"\u002Fprofiles\u002F{profile_id}\", response_model=ProfileResponse)\nasync def get_user_profile_by_id(profile: Mapping = Depends(valid_profile_id)):\n    \"\"\"Get profile by id.\"\"\"\n    return profile\n\n# src.creators.router.py\n@router.get(\"\u002Fcreators\u002F{profile_id}\", response_model=ProfileResponse)\nasync def get_user_profile_by_id(\n     creator_profile: Mapping = Depends(valid_creator_id)\n):\n    \"\"\"Get creator's profile by id.\"\"\"\n    return creator_profile\n\n```\n### FastAPI response serialization\nYou might think you can return a Pydantic object that matches your route's `response_model` and skip some processing steps, but you'd be wrong.\n\nFastAPI first converts the Pydantic object to a dict using `jsonable_encoder`, then validates the data against your `response_model`, and only then serializes it to JSON.\n\nThis means your Pydantic model object is created twice:\n- First, when you explicitly create it to return from your route.\n- Second, implicitly by FastAPI to validate the response data according to the response_model.\n\n```python\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, model_validator\n\napp = FastAPI()\n\n\nclass ProfileResponse(BaseModel):\n    @model_validator(mode=\"after\")\n    def debug_usage(self):\n        print(\"created pydantic model\")\n\n        return self\n\n\n@app.get(\"\u002F\", response_model=ProfileResponse)\nasync def root():\n    return ProfileResponse()\n```\n**Logs Output:**\n```\n[INFO] [2022-08-28 12:00:00.000000] created pydantic model\n[INFO] [2022-08-28 12:00:00.000020] created pydantic model\n```\n\n### If you must use sync SDK, then run it in a thread pool.\nIf you must use a library that's not `async`, run the HTTP calls in an external worker thread.\n\nUse `run_in_threadpool` from Starlette.\n```python\nfrom fastapi import FastAPI\nfrom fastapi.concurrency import run_in_threadpool\nfrom my_sync_library import SyncAPIClient \n\napp = FastAPI()\n\n\n@app.get(\"\u002F\")\nasync def call_my_sync_library():\n    my_data = await service.get_my_data()\n\n    client = SyncAPIClient()\n    await run_in_threadpool(client.make_request, data=my_data)\n```\n\n### BackgroundTasks vs a real task queue\nFastAPI's `BackgroundTasks` is convenient — and a footgun if you misuse it. Tasks run **after the response is sent, in the same worker process**. If the worker dies, the task is gone. There is no retry, no visibility, no scheduling.\n\n| Use `BackgroundTasks` when…                      | Use Celery \u002F Arq \u002F RQ when…                     |\n|--------------------------------------------------|-------------------------------------------------|\n| Task is short (\u003C 1 second)                       | Task takes seconds to minutes                   |\n| Failure can be silently dropped                  | You need retries or dead-letter handling        |\n| It's in-process (send email, log a row)          | It's CPU-heavy or needs a separate worker pool  |\n| You don't need scheduling or rate limiting       | You need cron, ETA, or rate limiting            |\n\n```python\nfrom fastapi import BackgroundTasks\n\n\n@router.post(\"\u002Fsignup\")\nasync def signup(data: SignupIn, bg: BackgroundTasks):\n    user = await service.create_user(data)\n    bg.add_task(send_welcome_email, user.email)  # fire-and-forget, in-process\n    return user\n```\nRule of thumb: if you'd page someone when the task is lost, it doesn't belong in `BackgroundTasks`.\n\n### ValueErrors might become Pydantic ValidationError\nIf you raise a `ValueError` in a Pydantic schema that's used directly in a request body, FastAPI will return a detailed validation error response to users.\n```python\n# src.profiles.schemas\nfrom pydantic import BaseModel, field_validator\n\nclass ProfileCreate(BaseModel):\n    username: str\n    password: str\n    \n    @field_validator(\"password\", mode=\"after\")\n    @classmethod\n    def valid_password(cls, password: str) -> str:\n        if not re.match(STRONG_PASSWORD_PATTERN, password):\n            raise ValueError(\n                \"Password must contain at least \"\n                \"one lower character, \"\n                \"one upper character, \"\n                \"digit or \"\n                \"special symbol\"\n            )\n\n        return password\n\n\n# src.profiles.routes\nfrom fastapi import APIRouter\n\nrouter = APIRouter()\n\n\n@router.post(\"\u002Fprofiles\")\nasync def create_profile(profile_data: ProfileCreate):\n   pass\n```\n**Response Example:**\n\n\u003Cimg src=\"images\u002Fvalue_error_response.png\" width=\"400\" height=\"auto\">\n\n### Docs\n1. Unless your API is public, hide docs by default. Show it explicitly on the selected envs only.\n```python\nfrom fastapi import FastAPI\nfrom starlette.config import Config\n\nconfig = Config(\".env\")  # parse .env file for env variables\n\nENVIRONMENT = config(\"ENVIRONMENT\")  # get current env name\nSHOW_DOCS_ENVIRONMENT = (\"local\", \"staging\")  # explicit list of allowed envs\n\napp_configs = {\"title\": \"My Cool API\"}\nif ENVIRONMENT not in SHOW_DOCS_ENVIRONMENT:\n   app_configs[\"openapi_url\"] = None  # set url for docs as null\n\napp = FastAPI(**app_configs)\n```\n2. Help FastAPI to generate an easy-to-understand docs\n   1. Set `response_model`, `status_code`, `description`, etc.\n   2. If models and statuses vary, use `responses` route attribute to add docs for different responses\n```python\nfrom fastapi import APIRouter, status\n\nrouter = APIRouter()\n\n@router.post(\n    \"\u002Fendpoints\",\n    response_model=DefaultResponseModel,  # default response pydantic model \n    status_code=status.HTTP_201_CREATED,  # default status code\n    description=\"Description of the well documented endpoint\",\n    tags=[\"Endpoint Category\"],\n    summary=\"Summary of the Endpoint\",\n    responses={\n        status.HTTP_200_OK: {\n            \"model\": OkResponse, # custom pydantic model for 200 response\n            \"description\": \"Ok Response\",\n        },\n        status.HTTP_201_CREATED: {\n            \"model\": CreatedResponse,  # custom pydantic model for 201 response\n            \"description\": \"Creates something from user request\",\n        },\n        status.HTTP_202_ACCEPTED: {\n            \"model\": AcceptedResponse,  # custom pydantic model for 202 response\n            \"description\": \"Accepts request and handles it later\",\n        },\n    },\n)\nasync def documented_route():\n    pass\n```\nWill generate docs like this:\n![FastAPI Generated Custom Response Docs](images\u002Fcustom_responses.png \"Custom Response Docs\")\n\n### Set DB keys naming conventions\nExplicitly setting the indexes' namings according to your database's convention is preferable over sqlalchemy's. \n```python\nfrom sqlalchemy import MetaData\n\nPOSTGRES_INDEXES_NAMING_CONVENTION = {\n    \"ix\": \"%(column_0_label)s_idx\",\n    \"uq\": \"%(table_name)s_%(column_0_name)s_key\",\n    \"ck\": \"%(table_name)s_%(constraint_name)s_check\",\n    \"fk\": \"%(table_name)s_%(column_0_name)s_fkey\",\n    \"pk\": \"%(table_name)s_pkey\",\n}\nmetadata = MetaData(naming_convention=POSTGRES_INDEXES_NAMING_CONVENTION)\n```\n### Migrations. Alembic\n1. Migrations must be static and reversible. If your migrations depend on dynamically generated data, make sure only the data itself is dynamic, not its structure.\n2. Generate migrations with descriptive names and slugs. The slug is required and should explain the changes.\n3. Set a human-readable file template for new migrations. We use the `*date*_*slug*.py` pattern, e.g., `2022-08-24_post_content_idx.py`\n```\n# alembic.ini\nfile_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s\n```\n### Set DB naming conventions\nBeing consistent with names is important. Some rules we followed:\n1. lower_case_snake\n2. singular form (e.g. `post`, `post_like`, `user_playlist`)\n3. group similar tables with module prefix, e.g. `payment_account`, `payment_bill`, `post`, `post_like`\n4. stay consistent across tables, but concrete namings are ok, e.g.\n   1. use `profile_id` in all tables, but if some of them need only profiles that are creators, use `creator_id`\n   2. use `post_id` for all abstract tables like `post_like`, `post_view`, but use concrete naming in relevant modules like `course_id` in `chapters.course_id`\n5. `_at` suffix for datetime\n6. `_date` suffix for date\n### SQL-first. Pydantic-second\n- Usually, database handles data processing much faster and cleaner than CPython will ever do. \n- It's preferable to do all the complex joins and simple data manipulations with SQL.\n- It's preferable to aggregate JSONs in DB for responses with nested objects.\n\nFor new projects, reach for SQLAlchemy 2.0's async API (`AsyncSession`, `async_sessionmaker`). The example below uses `encode\u002Fdatabases` for brevity — the SQL-first principle is what matters; the client is interchangeable.\n```python\n# src.posts.service\nfrom typing import Any\n\nfrom pydantic import UUID4\nfrom sqlalchemy import desc, func, select, text\nfrom sqlalchemy.sql.functions import coalesce\n\nfrom src.database import database, posts, profiles, post_review, products\n\nasync def get_posts(\n    creator_id: UUID4, *, limit: int = 10, offset: int = 0\n) -> list[dict[str, Any]]: \n    select_query = (\n        select(\n            (\n                posts.c.id,\n                posts.c.slug,\n                posts.c.title,\n                func.json_build_object(\n                   text(\"'id', profiles.id\"),\n                   text(\"'first_name', profiles.first_name\"),\n                   text(\"'last_name', profiles.last_name\"),\n                   text(\"'username', profiles.username\"),\n                ).label(\"creator\"),\n            )\n        )\n        .select_from(posts.join(profiles, posts.c.owner_id == profiles.c.id))\n        .where(posts.c.owner_id == creator_id)\n        .limit(limit)\n        .offset(offset)\n        .group_by(\n            posts.c.id,\n            posts.c.type,\n            posts.c.slug,\n            posts.c.title,\n            profiles.c.id,\n            profiles.c.first_name,\n            profiles.c.last_name,\n            profiles.c.username,\n            profiles.c.avatar,\n        )\n        .order_by(\n            desc(coalesce(posts.c.updated_at, posts.c.published_at, posts.c.created_at))\n        )\n    )\n    \n    return await database.fetch_all(select_query)\n\n# src.posts.schemas\nfrom typing import Any\n\nfrom pydantic import BaseModel, UUID4\n\n   \nclass Creator(BaseModel):\n    id: UUID4\n    first_name: str\n    last_name: str\n    username: str\n\n\nclass Post(BaseModel):\n    id: UUID4\n    slug: str\n    title: str\n    creator: Creator\n\n    \n# src.posts.router\nfrom fastapi import APIRouter, Depends\n\nrouter = APIRouter()\n\n\n@router.get(\"\u002Fcreators\u002F{creator_id}\u002Fposts\", response_model=list[Post])\nasync def get_creator_posts(creator: dict[str, Any] = Depends(valid_creator_id)):\n   posts = await service.get_posts(creator[\"id\"])\n\n   return posts\n```\n### Set tests client async from day 0\nWriting integration tests with DB will likely lead to messed up event loop errors in the future.\nSet the async test client immediately, using [httpx](https:\u002F\u002Fwww.python-httpx.org\u002F) with `ASGITransport`. Don't reach for `async_asgi_testclient` — it's unmaintained.\n\n```python\nfrom typing import AsyncGenerator\n\nimport pytest\nfrom httpx import AsyncClient, ASGITransport\n\nfrom src.main import app  # inited FastAPI app\n\n\n@pytest.fixture\nasync def client() -> AsyncGenerator[AsyncClient, None]:\n    transport = ASGITransport(app=app)\n    async with AsyncClient(transport=transport, base_url=\"http:\u002F\u002Ftest\") as ac:\n        yield ac\n\n\n@pytest.mark.asyncio\nasync def test_create_post(client: AsyncClient):\n    resp = await client.post(\"\u002Fposts\")\n\n    assert resp.status_code == 201\n```\n\n#### Override dependencies in tests\nDon't monkeypatch internals. FastAPI's `dependency_overrides` lets you swap any dependency for a test fake — auth, external clients, anything you don't want hitting the network.\n\n```python\nfrom src.auth.dependencies import parse_jwt_data\nfrom src.main import app\n\n\ndef fake_user():\n    return {\"user_id\": \"00000000-0000-0000-0000-000000000001\"}\n\n\n@pytest.fixture(autouse=True)\ndef _override_auth():\n    app.dependency_overrides[parse_jwt_data] = fake_user\n    yield\n    app.dependency_overrides.clear()\n```\n\nUnless you have synchronous database connections (excuse me?) or don't plan to write integration tests.\n\n### Use ruff\nWith linters, you can forget about formatting the code and focus on writing the business logic.\n\n[Ruff](https:\u002F\u002Fgithub.com\u002Fastral-sh\u002Fruff) is \"blazingly-fast\" new linter that replaces black, autoflake, isort, and supports more than 600 lint rules.\n\nIt's a popular good practice to use pre-commit hooks, but just using the script was ok for us.\n```shell\n#!\u002Fbin\u002Fsh -e\nset -x\n\nruff check --fix src\nruff format src\n```\n\n## Bonus Section\nSome very kind people shared their own experience and best practices that are definitely worth reading.\nCheck them out at [issues](https:\u002F\u002Fgithub.com\u002Fzhanymkanov\u002Ffastapi-best-practices\u002Fissues) section of the project.\n\nFor instance, [lowercase00](https:\u002F\u002Fgithub.com\u002Fzhanymkanov\u002Ffastapi-best-practices\u002Fissues\u002F4) \nhas described in details their best practices working with permissions & auth, class-based services & views, \ntask queues, custom response serializers, configuration with dynaconf, etc.  \n\nIf you have something to share about your experience working with FastAPI, whether it's good or bad, \nyou are very welcome to create a new issue. It is our pleasure to read it. \n","该项目总结了在创业公司中使用FastAPI框架的最佳实践和约定。核心功能和技术特点包括异步路由处理、广泛利用Pydantic进行数据验证与解析、依赖管理优化等，旨在提升开发效率和代码质量。特别适合于构建需要高并发处理能力的Web应用或微服务架构场景，也适用于希望遵循良好编码规范以促进项目长期维护性的团队。通过采用建议中的结构布局与设计模式，开发者可以创建出更加健壮且易于扩展的应用程序。",2,"2026-06-11 03:43:24","high_star"]