Версионирование API в Django: что ревьюить
Версионирование API — одно из тех решений, которые дёшево принять в начале и дорого переделывать потом. Разбираем, на что именно смотреть при ревью кода, связанного с версиями API в Django REST Framework: от выбора схемы до миграций сериализаторов.

Почему ревью версионирования — отдельная задача
Обычный pull request с новой вьюхой проверяют на стиль, покрытие тестами и корректность бизнес-логики. С версионированием API всё сложнее: ошибка не падает тестами сегодня — она ломает клиентов через три месяца, когда старую версию попытаются отключить, а выяснится, что половина мобильного трафика всё ещё на ней. Цена такой ошибки — вынужденная обратная совместимость на неопределённый срок или экстренный хотфикс в приложении, которое прошло ревью App Store.
Ниже — конкретные точки, на которые стоит смотреть при ревью. Материал написан для Django REST Framework 3.14+ (DRF), но принципы применимы к любому фреймворку, где версионирование реализуется на уровне роутинга или заголовков.
Схема версионирования: что проверять в настройках
DRF поддерживает несколько встроенных классов версионирования. Выбор схемы фиксируется один раз и меняется потом с болью — поэтому ревьюер должен убедиться, что решение осознанное, а не «взяли первое из документации».
| Схема (класс DRF) | Где живёт версия | Плюсы | Минусы |
|---|---|---|---|
| URLPathVersioning | В пути: /api/v1/orders/ | Очевидна для клиента, легко кешировать на CDN, удобна для документации | Дублирование URL-конфигурации, труднее переключать дефолтную версию |
| NamespaceVersioning | В namespace Django URL | Чистый urlconf, версия привязана к приложению | Неочевидна для фронтенда, ломает reverse() при ошибке в namespace |
| AcceptHeaderVersioning | В заголовке Accept | URL не меняется, соответствует духу HTTP content negotiation | Сложнее тестировать вручную, невидима в логах без настройки |
| QueryParameterVersioning | В query string: ?version=2 | Просто добавить | Ломает кеширование, выглядит как хак |
Что именно писать в комментарии к ревью
- «Почему выбрана именно эта схема?» — если в PR нет ни строчки обоснования, а проект новый, возвращайте. Решение должно быть задокументировано хотя бы в ADR или комментарии к конфигу.
- «DEFAULT_VERSION указан?» — без явного
DEFAULT_VERSIONв настройках DRF запрос без версии получитNone, и дальше поведение зависит от того, проверяет ли вьюхаrequest.version. Это мина. - «ALLOWED_VERSIONS перечислены?» — без этого списка клиент может запросить
v999и получить ответ от дефолтной версии, не узнав, что запрошенной не существует.
# settings.py — минимальный блок, который должен быть в PR
REST_FRAMEWORK = {
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_VERSION": "v1",
"ALLOWED_VERSIONS": ["v1", "v2"],
"VERSION_PARAM": "version",
}
Если в PR нет ни одного из этих четырёх ключей — это повод вернуть на доработку, а не «потом допилим».
Роутинг: где чаще всего ошибаются
При URLPathVersioning версия попадает в URL через параметр в urlpatterns. Типичная ошибка — подключить роутер один раз и забыть, что новый эндпоинт не попал в старую версию или, наоборот, попал, хотя не должен был.
Чек-лист для ревью роутинга
- Каждый
path()с параметром версии содержит явное ограничение:<str:version>— плохо,<version>через конвертер или regex с перечислением допустимых значений — лучше. Иначе/api/любая-строка/orders/вернёт 200. - Если вьюха доступна только в v2, она не должна быть зарегистрирована в urlconf v1. Проверьте, что роутеры разделены, а не один роутер подключён под двумя префиксами.
reverse()в тестах вызывается с явнымkwargs={"version": "v1"}. Без этого тест проходит сDEFAULT_VERSION, но не доказывает, что URL реально существует для нужной версии.
Самая частая проблема при ревью — не ошибка в коде, а отсутствие границ: непонятно, какие эндпоинты входят в какую версию, потому что роутер один на всех.
Сериализаторы: ветвление vs наследование
Когда появляется v2, возникает вопрос: как обслуживать два формата ответа. Два подхода — и у каждого свои ловушки при ревью.
Подход A: отдельный сериализатор на версию
OrderSerializerV1, OrderSerializerV2 — каждый описывает полный набор полей. Просто, но при десяти сериализаторах и трёх версиях получается тридцать файлов. При ревью проверяйте:
- Нет ли копипасты между версиями. Если V2 отличается от V1 одним полем, а скопировано 40 строк — это бомба: баг, исправленный в V1, не попадёт в V2.
- Тесты покрывают обе версии. Не «тестируем V2, V1 работает по аналогии».
Подход B: один сериализатор с ветвлением
Сериализатор проверяет self.context["request"].version и меняет набор полей. Компактнее, но при ревью проверяйте:
- Ветвление находится в
__init__илиget_fields(), а не разбросано поvalidate_*иto_representation. Иначе логика версий размазана, и следующий разработчик добавит v3 не в то место. requestточно есть в контексте. При вызове сериализатора из management-команды или Celery-задачи контекста нет — и вместо ошибки версионирования будетKeyErrorв три часа ночи.
Оба подхода валидны. Ключевое замечание при ревью — не «перепиши по-другому», а «в проекте уже есть принятый подход — новый PR ему соответствует?». Смешение двух подходов в одной кодовой базе — худший вариант.
Обратная совместимость: три красных флага
Версионирование существует ради обратной совместимости. Но PR может формально версию поднять, а совместимость сломать. Вот что искать:
- Удалённое поле в текущей версии. Поле убрали из V1-сериализатора — значит, все клиенты на V1 сломаются. Поле должно оставаться в V1, исчезая только в V2. Если автор PR убирает его из V1, это breaking change, замаскированный под рефакторинг.
- Изменённый тип поля.
priceбыло строкой, стало числом — в V2 допустимо, в V1 нет. Ревьюер должен найти diff в сериализаторе V1 и проверить, что типы не изменились. - Новый обязательный параметр в запросе. Если V1-эндпоинт начинает требовать новое поле в POST-теле, старые клиенты получат 400. Новые обязательные поля — только в новой версии.
Хорошая практика — попросить автора PR приложить таблицу: какие поля в какой версии присутствуют, какие обязательны. Это не бюрократия, а единственный способ увидеть картину целиком при десятках полей.
Тесты: что должно быть в PR
Тесты версионированного API отличаются от обычных. При ревью ищите:
- Тест на каждую поддерживаемую версию — не только на новую. Если в проекте три версии, а в PR тесты только для V3, верните.
- Тест на запрос без версии — должен вернуть ответ от
DEFAULT_VERSION, а не 404 или 500. - Тест на несуществующую версию — должен вернуть 404 или явную ошибку, а не тихо отработать как дефолтная.
- Тест на sunset-заголовок, если проект отмечает устаревшие версии (RFC 8594). Отсутствие этого заголовка — не баг, но если он задекларирован в API-контракте и пропущен в PR, это расхождение.
Минимальный тест, который должен быть в любом PR с новой версией:
# tests/test_versioning.py — DRF 3.14+, Django 4.2+
from rest_framework.test import APITestCase
from django.urls import reverse
class OrderVersioningTest(APITestCase):
def test_v1_returns_old_field(self):
url = reverse("order-list", kwargs={"version": "v1"})
response = self.client.get(url)
self.assertIn("total_price", response.data[0])
def test_v2_returns_new_field(self):
url = reverse("order-list", kwargs={"version": "v2"})
response = self.client.get(url)
self.assertIn("price_breakdown", response.data[0])
self.assertNotIn("total_price", response.data[0])
def test_unknown_version_returns_error(self):
url = "/api/v999/orders/"
response = self.client.get(url)
self.assertIn(response.status_code, [404, 406])
Документация и deprecation-политика
Технически это не код, но при ревью PR с новой версией стоит проверить:
- Обновлён ли OpenAPI-спецификация (или drf-spectacular-конфиг) — если схема генерируется автоматически, добавилась ли новая версия в
SPECTACULAR_SETTINGS? - Есть ли запись в changelog: что добавлено, что убрано, до какой даты старая версия поддерживается.
- Указан ли sunset-срок для предыдущей версии. Бессрочная поддержка пяти версий — это не версионирование, а коллекция легаси.
Смежный вопрос — как именно уведомлять клиентов об устаревании — отдельная тема. Здесь достаточно убедиться, что PR хотя бы фиксирует дату.
Что делать дальше
После ревью первого PR с версионированием зафиксируйте решения в ADR*: схема, подход к сериализаторам, политика sunset. Следующий ревьюер не должен переоткрывать те же вопросы. Полезно автоматизировать часть проверок: линтер на наличие ALLOWED_VERSIONS, CI-тест на запрос без версии, проверка OpenAPI-схемы на отсутствие v(N-2) после sunset-даты. Всё, что можно переложить на CI, не должно оставаться в голове ревьюера.
* ADR (Architecture Decision Record) — короткий документ, фиксирующий архитектурное решение: контекст, варианты, выбор и его обоснование. Хранится в репозитории рядом с кодом.
Автоэксперт / механик: Расшифровка OBD2-кодов, выбор б/у авто, диагностика по симптомам.
Спросить за 12 ₽Источники: Django REST Framework — Versioning (официальная документация), RFC 8594 — The Sunset HTTP Header Field
Частые вопросы
Какую схему версионирования выбрать, если проект только начинается?
Можно ли менять схему версионирования после запуска?
Сколько версий API поддерживать одновременно?
Что если в проекте уже есть API без версионирования?
Как быть, если у меня другая версия DRF и настройки не работают?
Какие подводные камни версионирования проявляются в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.