← назад к разделу

PYTHON · FASTAPI Фаза 5. Фундамент - ядро FastAPI

Обязательно

Структура приложения и конфигурация FastAPI

APIRouter · Lifespan · Settings

Доменные пакеты вместо файла на тысячу строк, ресурсы с владельцем и настройки, которые проверяются на старте.

1. APIRouter собирает маршруты по доменам

1 / 5

Products и orders живут в отдельных пакетах со своими маршрутами, логикой и моделями. Корневой app только подключает роутеры.

products.routerorders.routerFastAPI appinclude_router × 2

2. lifespan владеет ресурсами

2 / 5

До yield создаются пулы и клиенты, во время yield приложение работает, после yield ресурсы закрываются. Создание и очистка не разнесены по функциям.

Startupengine · HTTP clientyieldrequestsShutdowndispose · aclose

3. Settings проверяет окружение на старте

3 / 5

pydantic-settings читает APP_* и .env в одну типизированную модель. Обязательный секрет отсутствует - приложение падает сразу с понятной ошибкой.

Environment.envSettingsdatabase_url: strjwt_secret: SecretStrlru_cacheодин объект

4. Доменный пакет содержит всё своё

4 / 5

routes отвечает за HTTP, handlers за сценарий, repository за хранение, models за контракты. Переход между предметными областями не требует поиска по всему проекту.

app / ordersroutes.pyHTTPhandlers.pyuse caserepository.pydatabasemodels.pyschemasconfig.py и main.py остаются снаружи доменов

5. ASGI отличается от синхронного WSGI

5 / 5

FastAPI держит множество ожидающих соединений через async def. Django даёт ORM и админку из коробки, Flask - минимальный синхронный каркас.

FastAPIASGI · async defPydantic · DependsDjangoORM · admin · authвсё включеноFlaskWSGI · routesбиблиотеки на выбор

Минимальное FastAPI-приложение умещается в пять строк — и это обманчиво. Те же пять строк через полгода превращаются в один файл на тысячу строк, где роуты, конфигурация и подключение к базе перемешаны. Структура и конфигурация — это решения, которые стоит принять в первый день, потому что переделывать их потом долго и неприятно.

Здесь три отдельных вопроса: как разложить приложение на роутеры, как управлять ресурсами на старте и остановке, и как читать настройки. FastAPI даёт идиоматичный ответ на каждый.

Объект FastAPI: что это такое

Всё начинается с одной строки:

from fastapi import FastAPI

app = FastAPI(title="Catalog Service")

app — это корневой объект приложения. Он знает обо всех маршрутах, подключённых роутерах, жизненном цикле. Uvicorn (или любой другой ASGI-сервер) запускает именно его.

Класть все эндпоинты прямо на app не стоит — уже на втором-третьем домене (продукты, заказы, пользователи) файл станет нечитаемым. Для разделения есть APIRouter.

А где Django и Flask

На собеседовании почти всегда спросят, чем FastAPI отличается от двух старших фреймворков. Django это «всё включено»: ORM, миграции, админка, формы, аутентификация и шаблоны в одном пакете, и для сайта с админкой и десятком таблиц он даёт результат быстрее всех. Flask минимален: маршруты и запросы, остальное подключается библиотеками по вкусу. Оба исторически синхронные и работают через WSGI: один запрос занимает один поток или процесс целиком, и пока обработчик ждёт базу, этот процесс ничего больше не делает. Поэтому их запускают через gunicorn с несколькими рабочими процессами, а под тысячи одновременных соединений, долгие запросы и WebSocket они подходят плохо.

FastAPI построен на ASGI: обработчики могут быть async def, один процесс держит тысячи ожидающих соединений, а валидация и документация растут из аннотаций типов. Django с версии 3.0 тоже умеет ASGI и async-представления, но ORM и большинство библиотек остались синхронными, так что выигрыш частичный. Честный ответ про выбор: Django, когда нужна админка и скорость старта; FastAPI, когда это API под нагрузкой с внешними вызовами; Flask сегодня чаще в старых проектах и мелких внутренних инструментах.

Роутеры: одна ответственность на файл

APIRouter — это кусок маршрутов, объединённых общей темой. Каждый домен получает свой роутер со своим префиксом, а в конце они подключаются к корневому приложению.

# app/products/routes.py
from fastapi import APIRouter

router = APIRouter(prefix="/products", tags=["products"])

@router.get("/{product_id}")
async def get_product(product_id: int):
    ...
# main.py
from fastapi import FastAPI
from app.products.routes import router as products_router
from app.orders.routes import router as orders_router

app = FastAPI(title="Catalog Service")
app.include_router(products_router)
app.include_router(orders_router)

Каждый роутер живёт в своём пакете рядом со своей логикой — домены не растекаются друг в друга, и новый разработчик сразу понимает, где искать код по продуктам.

Жизненный цикл приложения: lifespan

Любому сервису нужны ресурсы, которые создаются один раз на старте и аккуратно закрываются при остановке: пул соединений к базе данных, HTTP-клиент к соседнему сервису, подключение к очереди сообщений.

старт код до yield: пул, HTTP-клиент, фабрика сессий работа yield: приложение принимает запросы SIGTERM код после yield: закрыть клиентов и пул

Создание и закрытие ресурсов стоят в одной функции по разные стороны yield, и забыть вторую половину трудно.

Раньше для этого использовали два отдельных обработчика:

@app.on_event("startup")
async def on_startup():
    app.state.engine = create_async_engine(DATABASE_URL)

@app.on_event("shutdown")
async def on_shutdown():
    await app.state.engine.dispose()

Проблема такого подхода: создание и закрытие ресурса разнесены по двум функциям — легко забыть закрыть то, что открыл.

Современный способ — lifespan. Это асинхронный контекст-менеджер: код до yield выполняется на старте, код после — на остановке. Создание и закрытие всегда рядом.

from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.config import get_settings

@asynccontextmanager
async def lifespan(app: FastAPI):
    settings = get_settings()
    app.state.engine = create_async_engine(settings.database_url)
    app.state.session_factory = async_sessionmaker(app.state.engine, expire_on_commit=False)
    yield
    await app.state.engine.dispose()

app = FastAPI(lifespan=lifespan)

Ресурсы кладут на app.state или в замыкание. Доставать их в эндпоинты лучше через зависимость, а не через глобальную переменную — подробнее об этом в статье про Dependency Injection.

Конфигурация: pydantic-settings

Плохая практика — разбросать os.getenv(...) по всему коду. Найти, где что читается, становится трудно, а если переменная не задана — приложение падёт в неожиданном месте.

pydantic-settings решает это: вы описываете все настройки как одну типизированную модель. Библиотека читает значения из переменных окружения и .env-файла, проверяет типы и валидирует на старте. Если обязательной переменной нет — приложение падает сразу при запуске с понятной ошибкой, а не посередине обработки запроса.

from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_")

    database_url: str
    debug: bool = False
    jwt_secret: str

@lru_cache
def get_settings() -> Settings:
    return Settings()

@lru_cache делает настройки одиночкой: модель строится один раз при первом вызове, потом возвращается кэшированный объект.

Профили окружения (локальный, тестовый, продовый) задаются не кодом, а тем, какие переменные окружения или какой .env-файл подан снаружи. Один и тот же класс Settings читает разные значения. Получать настройки в эндпоинтах стоит через зависимость:

from typing import Annotated
from fastapi import Depends

@router.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {"debug": settings.debug}

Так в тестах настройки легко подменить через app.dependency_overrides.

Как разложить код по файлам

FastAPI не навязывает структуру каталогов. Хорошо работает раскладка по доменам: каждый домен — отдельный пакет, внутри которого лежит всё, что к нему относится.

app/
  products/
    routes.py      # роутер и эндпоинты
    handlers.py    # бизнес-логика
    repository.py  # работа с базой
    models.py      # схемы запросов и ответов
  orders/
    routes.py
    handlers.py
    repository.py
    models.py
  config.py        # Settings и get_settings
main.py            # создание app и подключение роутеров

Такая раскладка держит домен собранным в одном месте. Когда нужно разобраться в заказах — идёшь в app/orders/, и всё нужное там.

Дополнительно: при первом чтении можно пропустить

Глубже: секреты, вложенные настройки и запускрасширенное

Секрет в настройках лучше объявить типом SecretStr: такое поле не попадёт в repr, логи и трассировки, а само значение достают явно через .get_secret_value(). Переменные окружения имеют приоритет над .env-файлом, поэтому в контейнере достаточно задать APP_DATABASE_URL, и файл для локальной разработки ему не помешает. Группы настроек описывают вложенными моделями и включают env_nested_delimiter="__": тогда APP_DB__POOL_SIZE=10 ляжет в settings.db.pool_size.

class DbSettings(BaseModel):
    url: str
    pool_size: int = 5

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", env_nested_delimiter="__")
    db: DbSettings
    jwt_secret: SecretStr

Запускают приложение ASGI-сервером: локально uvicorn app.main:app --reload, в бою uvicorn app.main:app --workers 4 или gunicorn с uvicorn-воркерами. Каждый воркер — отдельный процесс, и lifespan отрабатывает в каждом: четыре воркера означают четыре пула соединений к базе. В проде обычно прячут автодокументацию, если API не публичный: FastAPI(docs_url=None, redoc_url=None).

Коротко

  • FastAPI() — корневой объект приложения; все маршруты подключаются к нему.
  • APIRouter — группа маршрутов одного домена; каждый домен получает свой роутер со своим префиксом.
  • lifespan заменил @app.on_event — держит создание и закрытие ресурсов в одной функции, ничего не теряется.
  • pydantic-settings собирает все настройки в одну типизированную модель; если переменная не задана — приложение падает сразу на старте, а не в середине работы.
  • Профили (локальный, тест, прод) — это не код, а набор переменных окружения; класс Settings один.
  • Раскладка по доменным пакетам держит код организованным и не даёт доменам растекаться.
  • Секреты — через SecretStr, вложенные группы — через env_nested_delimiter; lifespan отрабатывает в каждом воркере uvicorn отдельно.
  • Django это полный набор с ORM и админкой на WSGI, Flask минимальный синхронный каркас, FastAPI это ASGI и async def: один процесс держит тысячи ожидающих соединений.

Что пощупать

Стартовый каталог практикума remodov/marketplace-system-python собирается функцией create_app(settings): Settings на pydantic-settings читает окружение, а lifespan поднимает движок, накатывает Alembic, создаёт фабрику сессий и кэш и кладёт сервис в app.state. Тест вызывает ту же функцию со своими настройками.

Код: config.py, main.py, db.py.

Что почитать дальше