Вы переименовали поле в JSON-ответе, выкатили релиз в пятницу, и уже через час в поддержке лавина: мобильное приложение показывает пустые экраны, а партнёрская интеграция падает с ошибкой парсинга. Знакомая история? Почти каждая команда проходит через неё хотя бы раз.
Проблема в том, что API — это контракт, а клиентов вы не контролируете. Пользователь iOS-приложения может не обновляться месяцами, а у партнёра релизный цикл занимает квартал. Поэтому версионирование API — не бюрократия, а способ развивать сервис, не останавливая чужой бизнес.
В этой статье разберём, почему breaking changes так дорого обходятся, какие есть стратегии версионирования и в чём их компромиссы. Покажем, как реализовать версионирование REST API на FastAPI через APIRouter с отдельной OpenAPI-схемой для каждой версии. Затем настроим версии в Django REST Framework через namespace и NamespaceVersioning.
Отдельно поговорим о политике устаревания: какие сроки поддержки закладывать, как использовать заголовки Deprecation и Sunset и как мигрировать клиентов без ультиматумов. Примеры написаны на Python 3.11+, FastAPI 0.110+ и DRF 3.15, но принципы применимы к любому стеку. Если коротко, цель такая: обратная совместимость API по умолчанию, новая версия только когда без неё нельзя.
Почему breaking changes ломают мобильные приложения и интеграции
Веб-фронтенд вы деплоите одновременно с бэкендом. С мобильными приложениями так не получится: после публикации в App Store и Google Play старые сборки живут в кармане у пользователей годами. По нашему опыту, спустя полгода после релиза до 15–25% аудитории всё ещё сидит на версиях, которые вышли до последних трёх обновлений.
Интеграции ещё менее предсказуемы. Партнёр написал клиент под ваш API два года назад, разработчик ушёл, документацию никто не открывал. Любое изменение для такого клиента — потенциальный простой.
Что именно считается breaking change:
- удаление или переименование поля в ответе;
- изменение типа данных (например,
idиз числа в строку); - добавление обязательного поля в запрос;
- изменение семантики статус-кодов или формата ошибок;
- ужесточение валидации, из-за которой раньше корректные запросы возвращают 422.
Безопасными обычно считаются добавление необязательных полей и новых эндпоинтов. Но и тут есть нюанс: строгие клиенты с генерируемыми моделями (например, на Kotlin с FAIL_ON_UNKNOWN_PROPERTIES) могут упасть даже от нового поля.
Принцип Постела работает в обе стороны: договоритесь с клиентами игнорировать неизвестные поля и зафиксируйте это в документации. Иначе «безопасные» изменения перестанут быть безопасными.
Цена ошибки измеряется не только часами простоя. Это отзывы в сторах, штрафы по SLA, срочные хотфиксы и потерянное доверие партнёров. Версионирование превращает такие инциденты в плановую работу. Если вы проектируете контракт с нуля, начните с проектирования API: правильные ресурсы и форматы ответов сокращают число будущих версий вдвое.
Стратегии версионирования: URL, заголовки, параметры запроса
Существует три основных подхода, и у каждого свои компромиссы. Выбор зависит от типа клиентов, инфраструктуры и зрелости команды.
1. Версия в URL (/api/v1/users). Самый популярный вариант. Он очевиден, легко кэшируется, удобно маршрутизируется на уровне nginx или API-шлюза и видна в логах. Минус: формально версия относится к ресурсу, а не к его представлению, и это нарушает пуристский REST. На практике большинству команд это не мешает.
2. Версия в заголовке (Accept: application/vnd.myapp.v2+json или X-API-Version: 2). Чистый URL и гибкость согласования контента. Но такие запросы сложнее отлаживать в браузере и через curl, а кэши должны учитывать Vary. Для публичных API это увеличивает порог входа.
3. Параметр запроса (/users?version=2). Просто в реализации и легко тестировать. Однако параметр часто теряется при проксировании, засоряет ключи кэша, а клиенты забывают его передавать и получают версию по умолчанию.
Сравнение по критериям: наглядность — URL выигрывает; чистота REST — заголовки; простота ручного тестирования — URL и параметр; удобство маршрутизации на шлюзе — URL.
Как выбирать? Порядок рассуждений обычно такой:
- Если API публичный или с мобильными клиентами — берите URL-версионирование.
- Если у вас строгий hypermedia-подход и контролируемые клиенты — рассмотрите заголовки.
- Параметр запроса оставьте для внутренних экспериментов.
- Версионируйте только мажорные изменения:
v1,v2, безv1.3.7в адресе.
Минорные изменения вносите в рамках текущей версии с сохранением совместимости. Для сложных систем с десятками сервисов стратегию версий стоит согласовать заранее, это частая тема на консультациях по микросервисной архитектуре.
Версионирование маршрутов в FastAPI: APIRouter и OpenAPI
FastAPI не навязывает схему версий, зато даёт отличный инструмент: APIRouter. Каждая версия — отдельный роутер со своими схемами Pydantic и обработчиками. Общую бизнес-логику выносим в сервисный слой, чтобы не дублировать код.
Базовая структура проекта выглядит так: app/api/v1/users.py, app/api/v2/users.py, app/services/users.py. Подключение:
from fastapi import FastAPI, APIRouter
from app.api.v1 import users as users_v1
from app.api.v2 import users as users_v2
app = FastAPI(title="Shop API")
v1 = APIRouter(prefix="/api/v1")
v1.include_router(users_v1.router, tags=["users"])
v2 = APIRouter(prefix="/api/v2")
v2.include_router(users_v2.router, tags=["users"])
app.include_router(v1)
app.include_router(v2)Чтобы у каждой версии была собственная документация, используйте подприложения через app.mount. Создайте api_v1 = FastAPI(title="Shop API v1"), подключите роутеры и смонтируйте: app.mount("/api/v1", api_v1). Тогда /api/v1/docs и /api/v1/openapi.json описывают только первую версию, а /api/v2/docs — вторую.
Для устаревших эндпоинтов задавайте deprecated=True в декораторе. Swagger UI зачеркнёт их, а сгенерированные клиенты получат пометку.
Схемы между версиями наследуйте осторожно. Если UserV2 наследует UserV1, изменение базовой модели незаметно затронет старый контракт. Надёжнее держать модели независимыми и покрывать контракт снапшот-тестами: сохраняйте openapi.json каждой версии и сравнивайте в CI, например через oasdiff. Падение сборки при изменении v1 — лучшая защита от случайных поломок. Подробнее о построении таких сервисов — на странице разработки на FastAPI.
Версионирование в Django REST Framework: namespace и класс версий
DRF содержит встроенную подсистему версионирования. Классы лежат в rest_framework.versioning: URLPathVersioning, NamespaceVersioning, AcceptHeaderVersioning, QueryParameterVersioning. Для публичных API чаще всего выбирают NamespaceVersioning: версия определяется пространством имён URL.
Настройки в settings.py:
REST_FRAMEWORK = {
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
"DEFAULT_VERSION": "v1",
"ALLOWED_VERSIONS": ["v1", "v2"],
}Маршруты в корневом urls.py:
urlpatterns = [
path("api/v1/", include(("users.urls_v1", "v1"), namespace="v1")),
path("api/v2/", include(("users.urls_v2", "v2"), namespace="v2")),
]Внутри представления доступен request.version. Самый простой путь — выбирать сериализатор по версии:
class UserViewSet(ModelViewSet):
def get_serializer_class(self):
if self.request.version == "v2":
return UserSerializerV2
return UserSerializerV1Если различий много, лучше развести представления по отдельным модулям, чем превращать код в лес условий. Для особых случаев напишите собственный класс, унаследовав BaseVersioning и переопределив determine_version, например чтобы брать версию из кастомного заголовка.
Укажите ALLOWED_VERSIONS обязательно: иначе клиент может запросить несуществующую версию и получить поведение по умолчанию. С белым списком DRF вернёт 404.
Не забывайте про документацию: drf-spectacular умеет генерировать схему под конкретную версию через параметр SPECTACULAR_SETTINGS и фильтрацию по namespace. Реализация таких решений на реальных проектах — часть нашей работы по разработке Django-приложений.
Политика устаревания: сроки, заголовки Deprecation и Sunset, миграция клиентов
Версий не должно быть бесконечно много: каждая стоит денег на поддержку, тесты и безопасность. Поэтому нужна публичная политика. Разумный стандарт для B2B и мобильных клиентов: поддерживать минимум две мажорные версии одновременно и давать 6–12 месяцев между объявлением об устаревании и отключением. Для внутренних сервисов хватит 1–3 месяцев.
Для машиночитаемых уведомлений есть два заголовка. Deprecation (RFC 9745) сообщает, что ресурс устарел, а Sunset (RFC 8594) — когда он будет отключён. Дополнительно используйте Link с rel="sunset" или rel="deprecation" на страницу миграции.
Пример для deprecation API в Django REST Framework — миксин:
class DeprecatedMixin:
sunset = "Sat, 31 Oct 2026 23:59:59 GMT"
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(request, response, *args, **kwargs)
response["Deprecation"] = "true"
response["Sunset"] = self.sunset
response["Link"] = '<https://example.com/docs/migrate-v2>; rel="deprecation"'
return responseВ FastAPI аналогичный эффект даёт middleware, которое добавляет заголовки к ответам с префиксом /api/v1.
План миграции клиентов можно оформить так:
- Опубликуйте changelog и руководство по переходу с примерами запросов.
- Включите заголовки
DeprecationиSunsetв ответы старой версии. - Логируйте использование по API-ключам и User-Agent, чтобы знать, кто ещё сидит на v1.
- Напишите активным клиентам лично за 90, 30 и 7 дней до отключения.
- Перед удалением на сутки включите «brownout»: кратковременные 410 Gone, чтобы разбудить забытые интеграции.
Метрики помогают принять решение. Если на v1 осталось менее 2% трафика и эти клиенты предупреждены, отключение безопасно. Настроить такие дашборды и алерты помогут специалисты по DevOps.
Ключевые выводы
- Новую версию создавайте только для breaking changes, остальное делайте обратно совместимо.
- URL-версионирование — оптимальный выбор для публичных и мобильных API; в FastAPI используйте APIRouter и mount, в DRF — NamespaceVersioning.
- Закрепите политику: две активные версии, 6–12 месяцев на миграцию, заголовки Deprecation и Sunset, мониторинг трафика по версиям.
Заключение
Версионирование API — это не про цифры в URL, а про уважение к тем, кто использует ваш сервис. Хорошая стратегия позволяет менять модель данных, чистить технический долг и внедрять новые функции, не заставляя клиентов бросать всё и срочно править код.
Начните с малого: выберите схему версий, зафиксируйте контракт текущей версии в OpenAPI и добавьте проверку совместимости в CI. Затем опишите политику устаревания и опубликуйте её в документации. Ещё через пару релизов вы заметите, что выкатка изменений перестала быть стрессом.
Помните о главном: лучшая версия API — та, которую не пришлось выпускать, потому что изменение удалось сделать обратно совместимым. А когда без v2 не обойтись, чёткие сроки, заголовки Deprecation и Sunset и честная коммуникация превращают болезненную миграцию в рядовую задачу.