Как разделить FastAPI на APIRouter и не удвоить /api/v1
После переноса обработчиков в отдельный файл путь неожиданно стал /api/v1/api/v1/items. Префикс версии записан и в APIRouter, и в include_router. FastAPI складывает эти части, поэтому структура файлов сама по себе не делает URL правильными.
Разделите локальный путь и адрес приложения
Роутер items знает свою область /items. Приложение при подключении добавляет общий /api/v1. Такая договорённость позволяет переиспользовать группу и видеть версию на одном уровне. В большом проекте возможна другая структура, но каждому уровню следует назначить понятную часть пути.
Функции обработчиков остаются обычными функциями; APIRouter собирает операции и метаданные до подключения к FastAPI. Не создавайте отдельный FastAPI в каждом модуле только ради разделения файлов: для обычной группы операций достаточно роутера. Отдельные приложения и mount нужны, когда действительно требуются отдельные границы приложения.
Проверим конечные пути, а не исходные строки
Сохраните пример в один файл, затем вынесите router и endpoints в модуль без изменения префиксов. Понадобятся FastAPI и HTTPX. Тест проверяет доступный URL, отсутствие удвоенного варианта и точное имя пути в OpenAPI.
Сохраните весь блок в файл и запустите Python 3.11+ в отдельном окружении с пакетами fastapi, httpx. Для моделей используется Pydantic v2; отдельный сервер для TestClient не нужен.
from fastapi import FastAPI, APIRouter
from fastapi.testclient import TestClient
items = APIRouter(prefix="/items", tags=["items"])
@items.get("/{item_id}", name="read_item")
def read_item(item_id: int):
return {"id": item_id}
app = FastAPI()
app.include_router(items, prefix="/api/v1")
with TestClient(app) as client:
assert client.get("/api/v1/items/7").json() == {"id": 7}
assert client.get("/api/v1/api/v1/items/7").status_code == 404
assert client.get("/items/7").status_code == 404
schema = client.get("/openapi.json").json()
assert set(schema["paths"]) == {"/api/v1/items/{item_id}"}
assert schema["paths"]["/api/v1/items/{item_id}"]["get"]["tags"] == ["items"]
print("router prefix and API version composed once")Как организовать файлы без циклических импортов
В модуле маршрутов создайте router и объявите операции. В точке входа импортируйте router и подключите к app. Модуль маршрутов не должен импортировать app обратно, иначе порядок загрузки станет зависеть от цикла. Общие схемы, зависимости и настройки вынесите в модули, которые не создают приложение при импорте.
Если всем операциям группы нужна проверка доступа, добавьте dependencies на уровне роутера или include_router. Но если обработчику нужен сам пользователь как значение, объявите Depends в аргументе либо общей зависимости. Установка зависимости только ради выполнения не передаёт её результат произвольным аргументам функции.
Что оставить после рефакторинга
Сохраните HTTP-тесты старых публичных адресов. Перенос файлов должен менять устройство проекта, а не неожиданно ломать URL. Сравнение OpenAPI до и после помогает заметить удалённый маршрут, задвоенный префикс и изменение имён операций для сгенерированного клиента.
Разделение по роутерам не обязывает делить каждую функцию в отдельный файл. Начните с групп ресурса и общих зависимостей. Когда модули становятся легче читать и тестировать, структура выполняет свою задачу. Если для простого GET нужно пройти цепочку из пяти промежуточных модулей без логики, полезно упростить границы.
Следующая практика
В открытом уроке первого GET вы соберёте минимальное приложение и увидите его Swagger. После этой базы перенесите маршрут в APIRouter и проверьте, что публичный путь и схема сохранились. Откройте бесплатный урок первого FastAPI-приложения.
В бесплатной программе FastAPI приложение постепенно получает модули, зависимости, базу и тесты. Такой путь позволяет усложнять структуру вместе с задачами, сохраняя рабочие HTTP-маршруты. Откройте программу курса.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
