Django и FastAPI являются наиболее популярными фреймворками для разработки web-приложений на Python. Действительно, в случае Django мы имеем отличный инструмент, позволяющий крайне быстро создать полноценное fullstack‑приложение, при этом имея из коробки огромное количество возможностей достаточно простых решений для основных типовых задач. В случае же FastAPI получаем гибкость, легковесность, высокую производительность и асинхронное ядро. А что, если для своего продукта мы хотим использовать лучшее от каждого фреймворка?

Как правило, для любого «продающего» веб‑продукта разрабатывается так называемая админка, с которой будут взаимодействовать внутренние сотрудники для ведения бизнеса. Мы знаем, что у Django админка есть из коробки, но не хотим лепить очередной Django‑монолит, который с течением времени приобретает все негативные качества любого монолита, но также имеет кучу специфических архитектурных особенностей — от синхронной ORM до «Vendor lock‑in» на уровне экосистемы. Но и делать админку кастомно через FastAPI для backend и ЧтоУгодноJS на frontend выглядит избыточным. Решение очевидно — разрабатываем внутреннюю админку на Django, а для всех прочих сервисов используем FastAPI. Что может пойти не так?

На деле такое решение требует реализации многих тонких моментов, игнорирование которых может обернуться если не катастрофой, то значительными трудностями в будущем. В этой статье я приведу некоторые практические рекомендации, которые позволят избежать головной боли и по-настоящему подружат Django и FastAPI так, что вы легко сможете реализовать их синергию в своем продукте!

Вы также можете посмотреть демонстрационный проект и ознакомиться с его исходным кодом: https://gitlab.com/Ergo_Sum/fastapi_and_django_services_lms_demo

Главный принцип: у данных должен быть один владелец

Первый и самый важный принцип такой: каждая таблица должна иметь одного владельца.

Если таблица принадлежит FastAPI-сервису, то:

  • только он определяет ее схему;

  • только он хранит миграции;

  • только он эволюционирует структуру таблицы;

  • Django может ее читать и даже редактировать записи, но не должен владеть ею на уровне schema management.

В моем демонстрационном проекте это реализовано через разделение по схемам PostgreSQL. user_service владеет схемой userscourse_service владеет схемой courses, а enrollment_service — схемой enrollments. Такое разделение делает разделение зон ответственности видимым на уровне самой базы. Когда таблицы всех сервисов лежат в public, границы быстро размываются. К тому же, разделение с помощью БД-схем оставляет возможность легко и просто масштабироваться, банально перенося, скажем, в новую базу только те модели, которые принадлежат одной или другой схеме, не трогая остальные.

Django в этой архитектуре подключается к той же базе, но работает с сервисными таблицами через unmanaged-модели. То есть модели в админке существуют, но для них указано managed = False. Это означает, что Django знает о таблицах, может их читать и отображать, но не пытается создавать, менять или удалять их своими миграциями. Так мы встраиваем Django-админку в микросервисный подход: админка может быть удобным интерфейсом над чужими данными, но не должна становиться владельцем этих данных.

Файл: backoffice/lms_admin/models.py

class UnmanagedModel(models.Model):
    class Meta:
        abstract = True
        managed = False

class Student(UnmanagedModel):
    id = models.BigIntegerField(primary_key=True)
    full_name = models.CharField(max_length=180)
    email = models.EmailField(max_length=180, unique=True)
    is_active = models.BooleanField(default=True)
    group_id = models.IntegerField()
    external_ref = models.CharField(max_length=64, unique=True)

    class Meta(UnmanagedModel.Meta):
        db_table = 'users"."students'

Связи

Вторая важная мысль состоит в том, что между доменами лучше не строить FK на уровне базы. В монолите естественно сделать связь enrollments.student_id к users.students.id, но в микросервисной архитектуре мы хотим изолировать таблицы так, чтобы миграциями каждой из них владел только один сервис. Очевидно, в нашем случае с разделением по схемам (аналогично при разделении по разным БД) между схемами передаются только идентификаторы. При этом внутри своей схемы FK вполне допустимы: например, у студента есть группа, у курса есть категория, у enrollment есть статус.

Файл: services/course_service/app/models.py

class Course(Base):
    __tablename__ = "courses"
    __table_args__ = {"schema": "courses"}

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    title: Mapped[str] = mapped_column(String(180))
    category_id: Mapped[int] = mapped_column(ForeignKey("courses.course_categories.id"))

Возникает естественный вопрос: не превратится ли такая админка в неудобный интерфейс с голыми student_id и course_id? На практике нет, если немного помочь Django admin. В формах можно использовать обычные choice-поля, чтобы вместо набора чисел оператор видел понятные значения. А в списках можно показывать вычисляемые поля, которые по идентификатору находят человекочитаемое имя. То есть архитектурно мы не строим жесткие связи между доменами, но интерфейс для людей все равно остается нормальным.

Файл: backoffice/lms_admin/forms.py

class EnrollmentAdminForm(forms.ModelForm):
    student_id = forms.TypedChoiceField(coerce=int, label="Student")
    course_id = forms.TypedChoiceField(coerce=int, label="Course")
    status_id = forms.TypedChoiceField(coerce=int, label="Status")

Миграции

С FastAPI сервисами все довольно несложно: каждый сервис мигрирует таблицы и данные только в свою схему. Однако, есть одна значительная проблема. Если несколько сервисов используют Alembic в одной базе, по умолчанию они конфликтуют из-за общей таблицы alembic_version. Поэтому каждому сервису нужна своя версия этой таблицы внутри своей схемы. Иначе второй сервис начнет путать миграционную историю первого. Это маленькая деталь, но именно на таких мелочах подобные архитектуры обычно и начинают рассыпаться при первом реальном запуске.

Файл: services/user_service/alembic/env.py

SERVICE_SCHEMA = "users"
VERSION_TABLE = "alembic_version"

context.configure(
    connection=connection,
    target_metadata=target_metadata,
    include_schemas=True,
    version_table=VERSION_TABLE,
    version_table_schema=SERVICE_SCHEMA,
)

Django при этом мигрирует только собственные служебные таблицы: auth, admin, contenttypes, sessions. Как только Django начинает мигрировать еще и сервисные таблицы, он перестает быть просто backoffice-слоем и начинает незаметно перетягивать зоны ответственности на себя. Тем не менее, админка может иметь и свои собственные таблицы. В таком случае есть три варианта реализации, и все они имеют право на жизнь:

  1. И служебные таблицы, и бизнесовые таблицы админки хранятся в схеме админки.

  2. Служебные таблицы хранятся в схеме public, а бизнесовые - в схеме админки.

  3. И служебные таблицы, и бизнесовые таблицы админки хранятся в схеме public.

Для локальной разработки в проекте есть отдельный migrator-сервис. Он обходит соседние каталоги FastAPI-сервисов, находит у них alembic.ini и последовательно применяет alembic upgrade head. Благодаря этому локальное окружение поднимается одной командой, а разработчику не нужно помнить ручной порядок запуска миграций. Для production это не обязательно единственный правильный вариант, но для демо-стенда, локальной разработки и onboarding-сценариев подход получается очень удобным.

Файл: migrator/run_migrations.py

def discover_alembic_projects() -> list[Path]:
    return sorted(path.parent for path in SERVICES_DIR.glob("*/alembic.ini"))
subprocess.run(
    [sys.executable, "-m", "alembic", "-c", "alembic.ini", "upgrade", "head"],
    cwd=service_dir,
    env=env,
    check=True,
)

Вывод

Django и FastAPI отлично уживаются водном продукте, если не пытаться сделать из них «полумонолит», в котором все могут все. Django хорош как быстрый backoffice. FastAPI хорош как слой доменных микросервисов. PostgreSQL‑схемы помогают провести реальную границу владения данными. А unmanaged‑модели позволяют админке работать с сервисными таблицами, не присваивая себе право управлять их жизненным циклом.

Описанные выше рекомендации отлично подходят как для создания нового микросервисного проекта (настроить Django‑админку выйдет дешевле поднятия полноценного внутреннего клиентского сервиса и соответствующего API для него), так и для распила монолита, скорее всего, изначально написанного на Django. Сами «правила» настройки такой связки фреймворков не только позволяют реализовать корректные взаимодействия между приложениями, но и приучает мыслить шире: учитывать варианты масштабирования, отказоустойчивости и общей инфраструктурной гибкости.

А главное, теперь нет чувства, что если в базе случится кризис, то с ним придется очень долго и усердно разбираться. Ведь теперь все разложено по полочкам! Точнее, по схемам.