Как разделить проект FastAPI на модули и не запутаться
Один файл main.py на 2 000 строк — верный способ получить конфликты при мерже и баги, которые не найти grep-ом. Разберём рабочую структуру модульного FastAPI-проекта: роутеры, зависимости, настройки и схемы — каждый в своём месте.

Когда один файл перестаёт работать
Пока в проекте три эндпоинта и одна модель, всё живёт в main.py без проблем. Но стоит добавить авторизацию, фоновые задачи, пару внешних интеграций — и файл разрастается до тысяч строк. Искать нужный роут приходится через Ctrl+F, а каждый мерж-реквест задевает половину файла. FastAPI с версии 0.1.0 поддерживает механизм APIRouter, который позволяет разнести эндпоинты по модулям. Ниже — конкретная структура, которую можно взять и перенести в свой проект на FastAPI 0.100+.
Что нужно до начала
- Python 3.10 или новее.
- FastAPI 0.100+ и Uvicorn — установленные в виртуальное окружение.
- Любой редактор с поддержкой Python (VS Code, PyCharm, даже vim с LSP).
- Базовое понимание декораторов маршрутов FastAPI:
@app.get,@app.post.
Целевая структура каталогов
Вот что получится в итоге. Каждый каталог — логический модуль: пользователи, товары, общие зависимости.
project/
├── app/
│ ├── __init__.py
│ ├── main.py # точка входа, подключение роутеров
│ ├── config.py # настройки через pydantic-settings
│ ├── dependencies.py # общие зависимости (сессия БД, текущий пользователь)
│ ├── users/
│ │ ├── __init__.py
│ │ ├── router.py # эндпоинты пользователей
│ │ ├── schemas.py # Pydantic-модели запроса и ответа
│ │ ├── service.py # бизнес-логика
│ │ └── models.py # ORM-модели (SQLAlchemy / другой маппер)
│ └── products/
│ ├── __init__.py
│ ├── router.py
│ ├── schemas.py
│ ├── service.py
│ └── models.py
└── pyproject.toml
Ключевой принцип: роутер знает только о схемах и сервисе своего модуля. Модуль users не импортирует ничего из products напрямую. Если нужна связь — она идёт через общий слой зависимостей или через сервис, который принимает нужный объект аргументом.
Шаг за шагом
1. Создайте каталоги и пустые __init__.py
mkdir -p app/users app/products
touch app/__init__.py app/users/__init__.py app/products/__init__.py
Проверка: команда find app -name '__init__.py' должна вернуть три файла.
2. Опишите настройки в config.py
# app/config.py
# FastAPI 0.100+, pydantic-settings 2.x
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "sqlite+aiosqlite:///./dev.db"
debug: bool = False
class Config:
env_file = ".env"
settings = Settings()
Проверка: python -c "from app.config import settings; print(settings.database_url)" — выведет строку подключения.
3. Создайте роутер модуля users
# app/users/schemas.py
from pydantic import BaseModel, EmailStr
class UserRead(BaseModel):
id: int
email: EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str
# app/users/router.py
from fastapi import APIRouter, status
from .schemas import UserCreate, UserRead
router = APIRouter(
prefix="/users",
tags=["users"], # группировка в Swagger UI
)
# Заглушка — замените на вызов service.create_user()
@router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def create_user(body: UserCreate):
"""Создать пользователя."""
return UserRead(id=1, email=body.email)
@router.get("/{user_id}", response_model=UserRead)
async def get_user(user_id: int):
"""Получить пользователя по ID."""
return UserRead(id=user_id, email="stub@example.com")
Обратите внимание: APIRouter принимает prefix — все пути внутри роутера будут начинаться с /users. Параметр tags нужен только для документации и не влияет на маршрутизацию.
4. Подключите роутер в main.py
# app/main.py
from fastapi import FastAPI
from app.users.router import router as users_router
from app.products.router import router as products_router # аналогичный файл
app = FastAPI(title="My modular API")
app.include_router(users_router)
app.include_router(products_router)
Проверка: запустите uvicorn app.main:app --reload и откройте http://127.0.0.1:8000/docs. В Swagger UI должны появиться группы users и products с соответствующими эндпоинтами.
5. Вынесите общие зависимости
# app/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> dict:
"""Проверить токен и вернуть данные пользователя."""
token = credentials.credentials
# Здесь — реальная валидация токена
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
)
return {"user_id": 1, "email": "user@example.com"}
Чтобы защитить все эндпоинты модуля разом, передайте зависимость в роутер:
# В app/users/router.py
from app.dependencies import get_current_user
router = APIRouter(
prefix="/users",
tags=["users"],
dependencies=[Depends(get_current_user)], # все маршруты защищены
)
Куда класть бизнес-логику
Частая ошибка — писать SQL-запросы и валидацию прямо в функции-эндпоинте. Через полгода тестировать такой код без поднятия HTTP-сервера невозможно. Выделяйте слой service.py в каждом модуле.
Роутер отвечает за HTTP: принять запрос, вернуть код ответа, сериализовать. Сервис отвечает за правила: можно ли создать, есть ли конфликт, что с чем связано. Если эти два слоя живут в одной функции, юнит-тесты превращаются в интеграционные.
Сервис импортируется в роутер, но сам ничего не знает ни о FastAPI, ни о Request, ни о Response. Он принимает обычные аргументы и возвращает данные или кидает исключение.
Сравнение подходов к структуре
| Критерий | Один файл | Модули по слоям (routes/, models/, …) | Модули по доменам (users/, products/) |
|---|---|---|---|
| Навигация при 20+ эндпоинтах | Тяжело | Средне — файл роутов всё равно большой | Легко — каждый модуль компактен |
| Конфликты при мерже | Постоянные | Реже, но routes.py страдает | Редкие — команды работают в разных каталогах |
| Связность модулей | Максимальная | Средняя: модель знает обо всех моделях | Низкая при дисциплине импортов |
| Переносимость модуля в другой сервис | Нет | Сложно | Копируете каталог и зависимости |
| Порог входа для новичка | Минимальный | Низкий | Чуть выше — нужно понять соглашения |
Для большинства проектов с двумя и более разработчиками третий вариант — деление по доменам — окупается уже через пару недель.
Если не получилось
ModuleNotFoundError: No module named 'app'
Uvicorn запущен не из корня проекта. Убедитесь, что вы стоите в каталоге project/ (том, где лежит app/), и запускаете uvicorn app.main:app.
ImportError: attempted relative import with no known parent package
Вы запустили файл напрямую: python app/users/router.py. Относительные импорты (from .schemas import …) работают только при запуске через пакет. Запускайте через Uvicorn или python -m app.main.
В Swagger нет нового роутера
Проверьте, что вызвали app.include_router(…) в main.py. Затем — что Uvicorn перезагрузился (флаг --reload). Частая причина: забытый импорт роутера.
Circular import
Модуль A импортирует из B, а B — из A. Обычно это значит, что общая зависимость (модель, утилита) должна жить уровнем выше — в app/dependencies.py или в отдельном модуле app/common/.
APIRouter — класс FastAPI, который позволяет группировать маршруты и подключать их к основному приложению через include_router(). По сути это мини-приложение без собственного сервера.
Что делать дальше
После разбиения на модули следующий шаг — добавить тесты. Каждый модуль тестируется независимо: сервис — через обычные pytest-функции, роутер — через httpx.AsyncClient (или TestClient из FastAPI). Убедитесь, что ни один тест сервиса не требует поднятия приложения целиком — это маркер того, что слои разделены правильно. Также стоит посмотреть на lifespan-контексты FastAPI для управления подключениями к БД и внешними ресурсами при старте и остановке — это отдельная тема, но она напрямую связана с тем, где именно создаются и закрываются соединения в модульной структуре.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: FastAPI — Bigger Applications – Multiple Files (официальная документация), FastAPI — APIRouter class reference
Частые вопросы
Можно ли вложить роутер в роутер?
Как быть, если два модуля зависят друг от друга?
А если у меня другая версия FastAPI — структура будет работать?
Нужно ли создавать отдельный модуль, если в нём один эндпоинт?
Будет ли такая структура работать с Alembic для миграций?
Какие подводные камни в проде при такой структуре?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.