APIRouter и структура проекта

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

APIRouter — это «мини-приложение» FastAPI: набор путей с собственными префиксом, тегами и зависимостями, который живёт в отдельном файле и подключается к основному приложению одной строкой app.include_router(...).

Учебные примеры почти всегда умещаются в один main.py, и это создаёт ложное ощущение, что так и надо. На практике через месяц у вас 60 эндпоинтов: пользователи, заказы, аутентификация, админка. Если всё свалено в один модуль, любое изменение — это прокрутка тысяч строк, конфликты в git у всей команды и страх что-нибудь задеть. APIRouter решает ровно эту боль: каждый раздел API становится самостоятельным файлом, который пишут и ревьюят независимо, а main.py превращается в короткую «точку сборки».

Первый роутер

Роутер объявляется почти как приложение, только вместо FastAPI()APIRouter(). Декораторы путей те же самые:

// app/routers/users.py
from fastapi import APIRouter

router = APIRouter()

@router.get("/users")
def list_users():
    return [{"id": 1, "name": "Ada"}]

@router.get("/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id, "name": "Ada"}

Сам по себе роутер ничего не обслуживает — у него нет сервера. Его нужно подключить к приложению:

// app/main.py
from fastapi import FastAPI
from app.routers import users

app = FastAPI()
app.include_router(users.router)

После этого GET /users и GET /users/{user_id} работают так, будто были объявлены прямо в main.py — но физически живут в своём файле.

Префиксы и теги

Повторять /users в каждом декораторе утомительно и хрупко. Префикс задаётся один раз — на уровне роутера или при подключении. Теги группируют эндпоинты в автодокументации /docs:

// app/routers/users.py
router = APIRouter(prefix="/users", tags=["users"])

@router.get("")          # это GET /users
def list_users(): ...

@router.get("/{user_id}")  # это GET /users/{user_id}
def get_user(user_id: int): ...

Префикс можно задать и в момент подключения — это удобно, когда один и тот же роутер монтируется в разные места или когда префикс зависит от версии API:

app.include_router(users.router, prefix="/api/v1")
// итог: /api/v1/users, /api/v1/users/{user_id}

Префикс обязан начинаться со слэша и не должен им заканчиваться — это частый источник путаницы. Теги же чисто документационные: каждый тег становится отдельной свёрнутой секцией в Swagger UI, поэтому осмысленные теги (users, orders, auth) сразу делают /docs читаемыми.

Структура папок большого приложения

Каноничная раскладка, которую рекомендует и сама документация FastAPI, — пакет app/ с подпакетом routers/:

app/
  __init__.py
  main.py            # создаёт FastAPI(), include_router'ы
  dependencies.py    # общие зависимости (БД, текущий пользователь)
  config.py          # настройки (Settings)
  routers/
    __init__.py
    users.py
    orders.py
    auth.py
  models/            # ORM-модели / схемы БД
  schemas/           # Pydantic-модели запросов и ответов
  services/          # бизнес-логика, не зависящая от HTTP

Главная идея — разделение ответственности: routers/ отвечают только за HTTP (разбор запроса, коды ответа), schemas/ описывают формат данных, а реальная логика живёт в services/ и не знает про FastAPI вовсе. Тогда бизнес-логику легко покрыть тестами без поднятия веб-сервера, а роутеры остаются тонкими.

Общие параметры на уровне роутера

APIRouter принимает не только префикс и теги, но и dependencies, responses, deprecated. Зависимости, заданные на роутере, применяются ко всем его путям — идеально для проверки прав на весь раздел:

from fastapi import APIRouter, Depends
from app.dependencies import verify_admin

router = APIRouter(
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(verify_admin)],   # на КАЖДЫЙ путь роутера
    responses={403: {"description": "Недостаточно прав"}},
)

Теперь любой эндпоинт внутри /admin автоматически защищён проверкой verify_admin — не нужно дописывать Depends в каждый обработчик и невозможно случайно забыть.

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

Внутри и FastAPI, и APIRouter хранят список объектов-маршрутов (routes). Когда вы вызываете app.include_router(users.router, prefix="/api"), FastAPI не «копирует ссылку» на роутер — он проходит по каждому маршруту дочернего роутера и регистрирует новый маршрут в приложении: склеивает префиксы (/api + /users), объединяет теги и конкатенирует списки зависимостей (зависимости приложения → роутера → пути выполнятся именно в этом порядке). Поэтому подключение — это разовая операция на старте: к моменту приёма запросов у приложения уже плоский, окончательный список путей, и роутеры как отдельные сущности в обработке запроса не участвуют. Отсюда практическое следствие: менять роутер после include_router бессмысленно — изменения не попадут в приложение, ведь маршруты уже скопированы.

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

  • Забыли app.include_router(...) — эндпоинты объявлены, но возвращают 404, потому что приложение про них не знает.
  • Задали префикс с завершающим слэшем (prefix="/users/") — пути получаются с двойным слэшем /users//{id}. Префикс начинается со слэша, но им не заканчивается.
  • Дважды указали префикс — и на APIRouter(prefix=...), и в include_router(prefix=...) — и удивляетесь пути /api/v1/users/users. Префиксы складываются.
  • Импортируют сам объект-функцию вместо роутера: include_router(users) вместо include_router(users.router) — ошибка типов.
  • Свалили бизнес-логику прямо в обработчики роутера — и потом не могут протестировать её без HTTP-клиента. Логику выносят в services/.

Итоги

  • APIRouter — это «мини-приложение»: набор путей в отдельном файле, подключаемый через app.include_router().
  • prefix задаёт общий путь (начинается со слэша, без слэша на конце), tags группируют эндпоинты в /docs.
  • Префикс можно задать на роутере или при подключении; при необходимости они складываются.
  • На уровне роутера задаются общие dependencies и responses — защита и документация для всех путей сразу.
  • Каноничная структура: routers/ (HTTP), schemas/ (данные), services/ (логика без FastAPI) — это упрощает тесты и поддержку.
Проверьте себя
1. В файле роутера написано `router = APIRouter(prefix="/users")`, а в main.py — `app.include_router(users.router, prefix="/api/v1")`. По какому пути будет доступен эндпоинт, объявленный как `@router.get("/{user_id}")`?
A/api/v1/users/{user_id}
B/users/{user_id}
C/api/v1/{user_id}
DБудет ошибка: префикс нельзя задавать в двух местах
2. Зачем передавать `dependencies=[Depends(verify_admin)]` в конструктор APIRouter, а не в каждый обработчик?
AТак зависимость применяется ко всем путям роутера сразу, и её нельзя случайно забыть на отдельном эндпоинте
BЗависимости в конструкторе роутера выполняются быстрее
CТолько так зависимость попадёт в документацию /docs
DВ обработчиках Depends в роутерах вообще не работает