В Spring внедрение зависимостей делает контейнер; во FastAPI его роль играет одна функция — Depends. Это не магия и не контейнер: зависимость — обычная функция (или класс), которую FastAPI вызывает за тебя перед эндпоинтом и результат передаёт аргументом. На Depends держится почти всё: доступ к базе, текущий пользователь, настройки, тестируемость.

Зависимость — это вызываемое

Зависимость объявляется через Annotated[Тип, Depends(функция)]. FastAPI видит её в сигнатуре, вызывает функцию и подставляет результат.

from typing import Annotated

from fastapi import Depends

from app.config import Settings, get_settings


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

Стиль с Annotated предпочтителен: тип читается явно, а саму зависимость легко переиспользовать. Часто её выносят в псевдоним типа, чтобы не повторять:

SettingsDep = Annotated[Settings, Depends(get_settings)]


@router.get("/info")
async def info(settings: SettingsDep):
    return {"debug": settings.debug}

Дерево зависимостей и под-зависимости

Зависимость сама может зависеть от других — FastAPI строит дерево и разрешает его сверху вниз. Это и есть способ собирать слои UCP-сервиса: репозиторий зависит от сессии, Handler — от репозитория, эндпоинт — от Handler-а.

def get_product_repository(session: SessionDep) -> ProductRepository:
    return ProductRepository(session)


def get_create_product_handler(
    repo: Annotated[ProductRepository, Depends(get_product_repository)],
) -> CreateProductHandler:
    return CreateProductHandler(repo)

Если одна зависимость встречается в дереве несколько раз, в пределах одного запроса FastAPI вызовет её один раз и переиспользует результат (кэширование по умолчанию). Поэтому сессию базы можно спокойно требовать и в репозитории, и где-то ещё — она будет одна на запрос.

yield-зависимости: ресурс с очисткой

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

from collections.abc import AsyncGenerator

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker


async def get_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
    session_factory = async_sessionmaker(request.app.state.engine, expire_on_commit=False)
    async with session_factory() as session:
        yield session


SessionDep = Annotated[AsyncSession, Depends(get_session)]

async with закрывает сессию после yield сам — это и есть очистка. Один сеанс живёт ровно один запрос; детали транзакций — в статье про персистентность.

Переопределение в тестах

Главная практическая выгода Depends: любую зависимость можно подменить в тестах через app.dependency_overrides, не трогая код эндпоинтов. Реальную базу — на тестовую, внешний клиент — на заглушку.

async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
    async with test_session_factory() as session:
        yield session


app.dependency_overrides[get_session] = get_test_session

Это то, ради чего стоит проводить через Depends всё, что обращается наружу: подменяемость встроена. Подробнее — в статье про тестирование.

Где проходит граница

Depends — это сборка объектов, а не место для бизнес-логики. Зависимость отвечает на вопрос «откуда взять X», а не «что с X сделать». Сценарий живёт в Handler-е, который зависимость лишь собирает и отдаёт. Если в зависимость потянуло логику ветвления по бизнес-правилам — это сигнал, что она должна быть в Handler-е.

Так Depends становится тем же, чем контейнер в Spring-биндинге: механизмом, который связывает слои UCP-сервиса, оставляя каждому одну ответственность. На нём держится и доступ к данным, и безопасность, и тестируемость — а значит, и способность продукт-инженера вести сервис в одиночку.