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) — это упрощает тесты и поддержку.