{"id":13424792,"url":"https://github.com/zhanymkanov/fastapi-best-practices","last_synced_at":"2025-05-14T00:11:25.615Z","repository":{"id":56851292,"uuid":"522822986","full_name":"zhanymkanov/fastapi-best-practices","owner":"zhanymkanov","description":"FastAPI Best Practices and Conventions we used at our startup","archived":false,"fork":false,"pushed_at":"2025-04-11T08:30:42.000Z","size":1684,"stargazers_count":11268,"open_issues_count":3,"forks_count":836,"subscribers_count":137,"default_branch":"master","last_synced_at":"2025-04-11T10:41:50.108Z","etag":null,"topics":["best-practices","fastapi"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zhanymkanov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-09T06:03:38.000Z","updated_at":"2025-04-11T09:35:10.000Z","dependencies_parsed_at":"2023-02-08T10:01:00.363Z","dependency_job_id":"572a221a-a28f-42e5-b91c-e08a78b09498","html_url":"https://github.com/zhanymkanov/fastapi-best-practices","commit_stats":{"total_commits":68,"total_committers":14,"mean_commits":4.857142857142857,"dds":"0.19117647058823528","last_synced_commit":"5555113176979c4f9773dedcf49e101031a76e7e"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhanymkanov%2Ffastapi-best-practices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhanymkanov%2Ffastapi-best-practices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhanymkanov%2Ffastapi-best-practices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhanymkanov%2Ffastapi-best-practices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zhanymkanov","download_url":"https://codeload.github.com/zhanymkanov/fastapi-best-practices/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254044419,"owners_count":22005157,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["best-practices","fastapi"],"created_at":"2024-07-31T00:00:59.559Z","updated_at":"2025-05-14T00:11:20.606Z","avatar_url":"https://github.com/zhanymkanov.png","language":null,"funding_links":[],"categories":["Resources","Others","Uncategorized","\u003e 100 ⭐️","miscellaneous","资源"],"sub_categories":["Best Practices","Uncategorized","指南"],"readme":"## FastAPI Best Practices \u003c!-- omit from toc --\u003e\nOpinionated list of best practices and conventions I use in startups.\n\nFor the last several years in production,\nwe have been making good and bad decisions that impacted our developer experience dramatically.\nSome of them are worth sharing. \n\n## Contents  \u003c!-- omit from toc --\u003e\n- [Project Structure](#project-structure)\n- [Async Routes](#async-routes)\n  - [I/O 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 \\\u0026 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  - [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 divide the project by file type (e.g., crud, routers, models), which works well for microservices or projects with fewer scopes. However, this approach didn't fit our monolith with many domains and modules.\n\nThe structure I found more scalable and evolvable for these cases is inspired by Netflix's [Dispatch](https://github.com/Netflix/dispatch), with some minor modifications.\n```\nfastapi-project\n├── alembic/\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/\n│   ├── auth\n│   ├── aws\n│   └── posts\n├── templates/\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/` - highest level of an app, contains common models, configs, and constants, etc.\n   2. `src/main.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 framework, in the first place. It is designed to work with async I/O operations and that is the reason it is so fast. \n\nHowever, FastAPI doesn't restrict you to use only `async` routes, and the developer can use `sync` routes as well. This might confuse beginner developers into believing that they are the same, but they are not.\n\n### I/O Intensive Tasks\nUnder the hood, FastAPI can [effectively handle](https://fastapi.tiangolo.com/async/#path-operation-functions) both async and sync I/O operations. \n- FastAPI runs `sync` routes in the [threadpool](https://en.wikipedia.org/wiki/Thread_pool) \nand blocking I/O operations won't stop the [event loop](https://docs.python.org/3/library/asyncio-eventloop.html) \nfrom executing the tasks. \n- If the route is defined `async` then it's called regularly via `await` \nand FastAPI trusts you to do only non-blocking I/O operations.\n\nThe caveat is if you fail that trust and execute blocking operations within async routes, \nthe event loop will not be able to run the next tasks until that blocking operation is done.\n```python\nimport asyncio\nimport time\n\nfrom fastapi import APIRouter\n\n\nrouter = APIRouter()\n\n\n@router.get(\"/terrible-ping\")\nasync def terrible_ping():\n    time.sleep(10) # I/O blocking operation for 10 seconds, the whole process will be blocked\n    \n    return {\"pong\": True}\n\n@router.get(\"/good-ping\")\ndef good_ping():\n    time.sleep(10) # I/O 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(\"/perfect-ping\")\nasync def perfect_ping():\n    await asyncio.sleep(10) # non-blocking I/O operation\n\n    return {\"pong\": True}\n\n```\n**What happens when we call:**\n1. `GET /terrible-ping`\n   1. FastAPI server receives a request and starts handling it \n   2. Server's event loop and all the tasks in the queue will be waiting until `time.sleep()` is finished\n      1. Server thinks `time.sleep()` is not an I/O task, so it waits until it is finished\n      2. Server won't accept any new requests while waiting\n   3. Server returns the response. \n      1. After a response, server starts accepting new requests\n2. `GET /good-ping`\n   1. FastAPI server receives a request and starts handling it\n   2. FastAPI sends the whole route `good_ping` to the threadpool, where a worker thread will run the function\n   3. While `good_ping` is being executed, event loop selects next tasks from the queue and works on them (e.g. accept new request, call db)\n      - Independently of main thread (i.e. our FastAPI app), \n        worker thread will be waiting for `time.sleep` to finish.\n      - Sync operation blocks only the side thread, not the main one.\n   4. When `good_ping` finishes its work, server returns a response to the client\n3. `GET /perfect-ping`\n   1. FastAPI server receives a request and starts handling it\n   2. FastAPI awaits `asyncio.sleep(10)`\n   3. Event loop selects next tasks from the queue and works on them (e.g. accept new request, call db)\n   4. When `asyncio.sleep(10)` is done, servers finishes the execution of the route and returns a response to the client\n\n\u003e [!WARNING]\n\u003e Notes on the thread pool:\n\u003e - Threads require more resources than coroutines, so they are not as cheap as async I/O operations.\n\u003e - 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://github.com/Kludex/fastapi-tips?tab=readme-ov-file#2-be-careful-with-non-async-functions) (external link)\n\n### CPU Intensive Tasks\nThe second caveat is that operations that are non-blocking awaitables or are sent to the thread pool must be I/O intensive tasks (e.g. open file, db call, external API call).\n- Awaiting CPU-intensive tasks (e.g. heavy calculations, data processing, video transcoding) is worthless since the CPU has to work to finish the tasks, \nwhile I/O operations are external and server does nothing while waiting for that operations to finish, thus it can go to the next tasks.\n- Running CPU-intensive tasks in other threads also isn't effective, because of [GIL](https://realpython.com/python-gil/). \nIn short, GIL allows only one thread to work at a time, which makes it useless for CPU tasks. \n- If you want to optimize CPU intensive tasks you should send them to workers in another process.\n\n**Related StackOverflow questions of confused users**\n1. https://stackoverflow.com/questions/62976648/architecture-flask-vs-fastapi/70309597#70309597\n   - Here you can also check [my answer](https://stackoverflow.com/a/70309597/6927498)\n2. https://stackoverflow.com/questions/65342833/fastapi-uploadfile-is-slow-compared-to-flask\n3. https://stackoverflow.com/questions/71516140/fastapi-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 regular features like required \u0026 non-required fields with default values, \nPydantic has built-in comprehensive data processing tools like regex, enums, strings manipulation, emails validation, etc.\n```python\nfrom enum import Enum\nfrom pydantic import AnyUrl, BaseModel, EmailStr, Field\n\n\nclass MusicBand(str, Enum):\n   AEROSMITH = \"AEROSMITH\"\n   QUEEN = \"QUEEN\"\n   ACDC = \"AC/DC\"\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, default=None)  # must be greater or equal to 18\n    favorite_band: MusicBand | None = None  # only \"AEROSMITH\", \"QUEEN\", \"AC/DC\" 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 zoneinfo import ZoneInfo\n\nfrom fastapi.encoders import jsonable_encoder\nfrom pydantic import BaseModel, ConfigDict\n\n\ndef datetime_to_gmt_str(dt: datetime) -\u003e str:\n    if not dt.tzinfo:\n        dt = dt.replace(tzinfo=ZoneInfo(\"UTC\"))\n\n    return dt.strftime(\"%Y-%m-%dT%H:%M:%S%z\")\n\n\nclass CustomModel(BaseModel):\n    model_config = ConfigDict(\n        json_encoders={datetime: datetime_to_gmt_str},\n        populate_by_name=True,\n    )\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 was a great innovation for reading environment variables, but having a single BaseSettings for the whole app can become messy over time. To improve maintainability and organization, we have split the BaseSettings across different 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, model_validator\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 involve calling a database or external services, it is not sufficient.\n\nFastAPI documentation mostly presents dependencies as DI for endpoints, but they are also excellent for request validation.\n\nDependencies can be used to validate data against database constraints (e.g., checking if an email already exists, ensuring a user is found, etc.).\n```python\n# dependencies.py\nasync def valid_post_id(post_id: UUID4) -\u003e 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(\"/posts/{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(\"/posts/{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(\"/posts/{post_id}/reviews\", 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 to dependency, we would have to validate `post_id` exists\nfor 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 the similar logic.\n```python\n# dependencies.py\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError, jwt\n\nasync def valid_post_id(post_id: UUID4) -\u003e 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=\"/auth/token\"))\n) -\u003e dict[str, Any]:\n    try:\n        payload = jwt.decode(token, \"JWT_SECRET\", algorithms=[\"HS256\"])\n    except JWTError:\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) -\u003e 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(\"/users/{user_id}/posts/{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 \u0026 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\nfrom jose import JWTError, jwt\n\nasync def valid_post_id(post_id: UUID4) -\u003e 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=\"/auth/token\"))\n) -\u003e dict:\n    try:\n        payload = jwt.decode(token, \"JWT_SECRET\", algorithms=[\"HS256\"])\n    except JWTError:\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) -\u003e 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(\"/users/{user_id}/posts/{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, and there is a temptation to use `sync` dependencies, when you don't have to await anything, but that might not be the best choice.\n\nJust as with routes, `sync` dependencies are run in the thread pool. And threads here also come with a price and limitations, that are redundant, if you just make a small non-I/O operation.\n\n[See more](https://github.com/Kludex/fastapi-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 /courses/:course_id`\n   2. `GET /courses/:course_id/chapters/:chapter_id/lessons`\n   3. `GET /chapters/:chapter_id`\n\nThe only caveat is to use the same variable names in the path:\n- If you have two endpoints `GET /profiles/:profile_id` and `GET /creators/:creator_id`\nthat both validate whether the given `profile_id` exists,  but `GET /creators/: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) -\u003e 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)) -\u003e Mapping:\n    if not profile[\"is_creator\"]:\n       raise ProfileNotCreator()\n\n    return profile\n\n# src.profiles.router.py\n@router.get(\"/profiles/{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(\"/creators/{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\nIf you think you can return Pydantic object that matches your route's `response_model` to make some optimizations,\nthen it's wrong. \n\nFastAPI firstly converts that pydantic object to dict with its `jsonable_encoder`, then validates \ndata with your `response_model`, and only then serializes your object to JSON. \n```python\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, root_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(\"/\", 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 to interact with external services, and it's not `async`,\nthen make the HTTP calls in an external worker thread.\n\nWe can use the well-known `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(\"/\")\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### ValueErrors might become Pydantic ValidationError\nIf you raise a `ValueError` in a Pydantic schema that is directly faced by the client, it will return a nice detailed response to users.\n```python\n# src.profiles.schemas\nfrom pydantic import BaseModel, field_validator\n\nclass ProfileCreate(BaseModel):\n    username: str\n    \n    @field_validator(\"password\", mode=\"after\")\n    @classmethod\n    def valid_password(cls, password: str) -\u003e 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(\"/profiles\")\nasync def get_creator_posts(profile_data: ProfileCreate):\n   pass\n```\n**Response Example:**\n\n\u003cimg src=\"images/value_error_response.png\" width=\"400\" height=\"auto\"\u003e\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    \"/endpoints\",\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/custom_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 revertable.\nIf your migrations depend on dynamically generated data, then \nmake sure the only thing that is dynamic is the data itself, not its structure.\n2. Generate migrations with descriptive names \u0026 slugs. Slug is required and should explain the changes.\n3. Set human-readable file template for new migrations. We use `*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```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) -\u003e 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(\"/creators/{creator_id}/posts\", 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 most likely lead to messed up event loop errors in the future.\nSet the async test client immediately, e.g. [httpx](https://github.com/encode/starlette/issues/652)\n```python\nimport pytest\nfrom async_asgi_testclient import TestClient\n\nfrom src.main import app  # inited FastAPI app\n\n\n@pytest.fixture\nasync def client() -\u003e AsyncGenerator[TestClient, None]:\n    host, port = \"127.0.0.1\", \"9000\"\n\n    async with AsyncClient(transport=ASGITransport(app=app, client=(host, port)), base_url=\"http://test\") as client:\n        yield client\n\n\n@pytest.mark.asyncio\nasync def test_create_post(client: TestClient):\n    resp = await client.post(\"/posts\")\n\n    assert resp.status_code == 201\n```\nUnless you have sync db connections (excuse me?) or aren't planning 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://github.com/astral-sh/ruff) 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#!/bin/sh -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://github.com/zhanymkanov/fastapi-best-practices/issues) section of the project.\n\nFor instance, [lowercase00](https://github.com/zhanymkanov/fastapi-best-practices/issues/4) \nhas described in details their best practices working with permissions \u0026 auth, class-based services \u0026 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","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzhanymkanov%2Ffastapi-best-practices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzhanymkanov%2Ffastapi-best-practices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzhanymkanov%2Ffastapi-best-practices/lists"}