Переход с Django на FastAPI: чего ждать и где будет больно
Переезд с Django на FastAPI — это не просто смена фреймворка. Это смена философии: из тёплого монолита со встроенным ORM, админкой и middleware-ms ты попадаешь в минималистичный микрофреймворк, где почти всё нужно собрать самому. Я через это прошёл четыре года назад на одном из своих сервисов, потом ещё дважды — на новых проектах. Расскажу, чего ждать, какие рефлексы переучивать и где реально станет лучше, а где — наоборот.
Что отличается на уровне идеологии
Django — это всё-в-одном: ORM, formы, шаблоны, аутентификация, middleware, админка, миграции. Ты получаешь огромную полезную нагрузку, но платишь зависимостью от Django-way: ORM знает про request, форма знает про модель, всё связано.
FastAPI ничего не знает про БД, ничего не знает про админку, ничего не знает про сессии. Это просто фреймворк для HTTP-эндпоинтов поверх Starlette. ORM ты приносишь свой (обычно SQLAlchemy 2 + Alembic), сессии — свои, аутентификацию — свою.
Это и плюс, и минус. Плюс: гибкость, явность, тестируемость. Минус: первое время ощущение, что ты пишешь код, который раньше был «просто настройкой».
ORM: SQLAlchemy 2 вместо Django ORM
Django ORM — это lazy-фабрика QuerySet, которая делает всё неявно. User.objects.filter(active=True) — и под капотом одна абстракция, поверх неё другая, поверх ещё одна. Удобно, пока тебе хватает простых случаев. Когда не хватает — лезешь в raw, потом в extra, потом просто пишешь SQL руками.
SQLAlchemy 2 в async-режиме другой:
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
async def get_active_users(session: AsyncSession):
stmt = select(User).where(User.is_active == True)
result = await session.execute(stmt)
return result.scalars().all()
Запрос строишь явно. Никаких неявных JOIN от select_related: хочешь join — пишешь options(selectinload(User.posts)). На первый взгляд многословно. Через месяц понимаешь, что видишь, что именно уходит в БД, и это бесценно. Никаких сюрпризов с N+1.
Главное психологическое препятствие — отучиться от obj.related_set.all() внутри обработчика. В async это либо ошибка lazy loading, либо неявный sync-вызов. В SQLAlchemy ты грузишь связи заранее или явно делаешь второй запрос.
Миграции: Alembic вместо встроенных
Django-миграции — гениальная штука: makemigrations, и фреймворк сам видит изменения в моделях. Alembic делает то же самое через autogenerate, но без той интеграции: ты должен вручную указать target_metadata, проверять результат глазами, иногда дописывать.
Файл alembic/env.py в реальном проекте выглядит так:
from app.db.models import Base
target_metadata = Base.metadata
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
После настройки работает не хуже Django-миграций. Просто стоимость старта выше.
Аутентификация и сессии
В Django-сессии и аутентификация работают «из коробки»: middleware прицепляет request.user, есть встроенный login_required. В FastAPI ничего такого нет.
Стандартный путь — JWT-токены или сессии в Redis. Из готовых пакетов — fastapi-users, fastapi-jwt-auth, но мы у себя не используем: проще написать своё под конкретные требования.
from fastapi import HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
async def get_current_user(
token: str = Depends(oauth2_scheme),
session: AsyncSession = Depends(get_session),
) -> User:
user_id = decode_jwt(token)
user = await session.get(User, user_id)
if not user:
raise HTTPException(401, "invalid token")
return user
Получился тонкий явный кусок. Тестируется без боли — переопределил get_current_user в dependency_overrides, и готово. Минус — это ещё пара десятков строк, которые в Django ты не писал.
Админка: главное, чего нет
Django admin — мощная штука. В FastAPI её нет, и в этом самый большой удар по продуктивности на старте. Альтернативы:
sqladmin— отдельный пакет, ставится поверх FastAPI, читает SQLAlchemy-модели, генерирует CRUD. Не такая мощная, как Django admin, но базовое CRUD-управление дает.fastapi-admin— конкурент, работает с Tortoise ORM. Если у тебя SQLAlchemy — не подходит.- Обычное API + отдельный фронт-админ. Стандартный подход для крупных проектов: ты не делаешь админку под каждую модель, ты делаешь её один раз как нормальный фронт.
На небольших внутренних сервисах sqladmin закрывает потребность. На больших проектах админкой занимается фронт-команда отдельно, и это даже лучше — она получается под пользователей, а не под разработчиков.
Middleware и сигналы
Django-сигналы и middleware у разработчиков любимы и нелюбимы примерно поровну. В FastAPI middleware есть, но это просто Starlette middleware:
@app.middleware("http")
async def add_request_id(request, call_next):
request.state.request_id = uuid4().hex
response = await call_next(request)
response.headers["X-Request-ID"] = request.state.request_id
return response
А вот сигналов в стиле Django нет. И, по правде говоря, без них живётся легче. Сигналы — это неявная связь: ты не видишь в коде модели, что при save идёт сторонний обработчик. В FastAPI всё явно: хочешь побочный эффект — вызывай сервисный метод, не думай, что «где-то прицеплен сигнал».
Async: главная причина, ради которой переходят
Главное преимущество FastAPI — нативный async. Когда у тебя сервис ходит в БД, в Redis, в три внешних API на каждый запрос, синхронный Django съедает воркеров. На async всё это идёт параллельно, и один воркер обслуживает в разы больше клиентов.
Но не каждый код от перехода на FastAPI становится быстрее. Если ты делаешь один синхронный запрос в БД и больше ничего — async тебе не поможет, разница будет в пределах погрешности. Async окупается там, где много ожиданий внешних ресурсов.
Что сложнее
Несколько вещей, которые в Django делались тривиально:
- Формы и валидация загрузки файлов. Django formы умели многое. В FastAPI ты получаешь pydantic-модели для JSON и руками собираешь обработку
multipart/form-data. - Шаблоны. Если делал серверный рендер — добавляешь Jinja2 руками. В Django это было встроено.
- Permissions. Django-permissions завязаны на ORM и ContentType. В FastAPI ты пишешь свою систему прав. Обычно это к лучшему — система получается под твою реальную модель.
- Менеджмент-команды.
./manage.py custom_command— нет такого. Берёшьtyperилиclick, пишешь свой CLI.
Что однозначно лучше
- OpenAPI «из коробки». Никаких отдельных
drf-yasgилиdrf-spectacular— просто пишешь типы, и схема готова. - Тестируемость.
dependency_overrides— лучшая система тестового подмена, что я видел. - Производительность. На реальных нагрузках async-FastAPI делает sync-Django в разы по throughput на одной ноде.
- Минимализм. Меньше магии, больше явного кода. Через год возвращаешься в проект и понимаешь, что происходит, без чтения исходников фреймворка.
Стоит ли переходить
Если у тебя работающий Django-сервис, который не упирается в производительность и удовлетворяет потребности — не переходи. Переход на FastAPI — это переписывание, и на середине пути ты можешь обнаружить, что Django-вая тебе нравилась больше. Сначала надо понять, чего тебе не хватает.
Если строишь новый сервис, который должен много общаться с внешними системами и держать высокий RPS — FastAPI хороший выбор. Будь готов потратить первые недели на «склейку» того, что в Django было готово, но дальше станет легче и чище.
Я не жалею о переходе. Но и не ностальгирую: задачи, которые лучше решает Django (контентные сайты с админкой, классические CRUD-приложения) — пусть остаются в Django.