Dependency Injection и Depends

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

Суть DI: вы не создаёте нужные объекты внутри обработчика, а объявляете их как параметры. FastAPI выстраивает граф зависимостей и наполняет его за вас перед вызовом функции.

Во многих эндпоинтах повторяется одинаковая подготовка: проверить токен, открыть сессию БД, разобрать общие query-параметры пагинации. Копировать этот код в каждый обработчик — путь к ошибкам. FastAPI предлагает вынести его в отдельные функции-«зависимости» и подключать их через Depends. Обработчик просто говорит «мне нужен результат вот этой функции», и фреймворк его предоставляет.

from fastapi import FastAPI, Depends
from typing import Annotated

app = FastAPI()

# зависимость: общие параметры пагинации
def pagination(skip: int = 0, limit: int = 20):
    return {"skip": skip, "limit": limit}

PageParams = Annotated[dict, Depends(pagination)]

@app.get("/items")
async def list_items(page: PageParams):
    return {"page": page}

@app.get("/users")
async def list_users(page: PageParams):
    return {"page": page}

Здесь функция pagination — зависимость; оба эндпоинта получают её результат, не дублируя разбор параметров. Современный стиль — завести псевдоним типа Annotated[dict, Depends(pagination)] и переиспользовать его. Зависимости сами могут принимать параметры запроса (как skip/limit здесь) — FastAPI разберёт их так же, как у обычного обработчика.

Как работает под капотом

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

def get_db():
    return "db-session"

def get_user(db):                 # зависит от get_db
    return f"user(from {db})"

def handler(db, user):            # зависит от обоих
    return f"ответ: {user}, {db}"

# граф зависимостей: что от чего зависит
graph = {
    "db":      (get_db,   []),
    "user":    (get_user, ["db"]),
    "handler": (handler,  ["db", "user"]),
}

cache = {}
def resolve(name):
    if name in cache:             # кэш в пределах одного запроса
        return cache[name]
    func, deps = graph[name]
    args = [resolve(d) for d in deps]   # сперва вычисляем зависимости
    cache[name] = func(*args)
    return cache[name]

print(resolve("handler"))
print("db вычислялся один раз:", cache["db"])

Попробуй сам ▶ Обрати внимание: db вычисляется один раз и переиспользуется обеими зависимостями — так же FastAPI кэширует зависимости внутри запроса.

Частые ошибки

Первая — создавать ресурсы (соединение с БД, клиент) прямо в обработчике вместо зависимости, теряя переиспользование и единое управление. Вторая — забывать, что результат зависимости кэшируется в пределах запроса: одна и та же зависимость не вычислится дважды (если не отключить кэш через use_cache=False). Третья — городить слишком глубокие графы зависимостей, усложняя отладку.

Best practices

  • Выносите повторяющуюся подготовку (пагинация, текущий пользователь, сессия) в зависимости.
  • Заводите псевдонимы типов Annotated[T, Depends(...)] и переиспользуйте их.
  • Полагайтесь на кэш зависимостей в пределах запроса; отключайте его осознанно.
  • Держите зависимости маленькими и сфокусированными — одну ответственность на зависимость.

Зачем DI, если можно просто вызвать функцию

Резонный вопрос новичка: зачем Depends, если общую логику можно вынести в обычную функцию и вызывать её руками в начале каждого обработчика? Отличие в четырёх вещах. Во-первых, зависимость сама участвует в разборе запроса: она может объявлять свои path/query/header-параметры, и FastAPI наполнит их. Во-вторых, результат кэшируется в пределах запроса, так что одна сессия БД не создастся пять раз. В-третьих, зависимости попадают в OpenAPI-схему: требования вроде токена видны в документации. В-четвёртых, их можно подменять в тестах через dependency_overrides, не трогая код обработчиков. Ручной вызов функции не даёт ничего из этого. Поэтому DI — не усложнение ради красоты, а механизм, который одновременно решает разбор параметров, переиспользование, документирование и тестируемость. Это инвестиция, окупающаяся на каждом эндпоинте.

Итог: DI позволяет объявлять потребности обработчика как параметры. FastAPI строит граф зависимостей, вычисляет их (с кэшем в пределах запроса) и подставляет результаты — это убирает дублирование и централизует общую логику.

Проверьте себя
1. Что делает FastAPI, встретив параметр page: Annotated[dict, Depends(pagination)]?
AИгнорирует его
BВызывает функцию pagination, разобрав её параметры из запроса, и подставляет результат в page
CТребует, чтобы клиент прислал поле page
DСоздаёт новый поток
2. Сколько раз одна и та же зависимость вычисляется в пределах одного запроса по умолчанию?
AПо разу для каждого использования
BОдин раз — результат кэшируется и переиспользуется
CНи разу, пока её явно не вызовут
DБесконечно, в цикле