Представь: ты переименовал поле user_name в username в ответе API. Казалось бы, мелочь. Но через час начинают сыпаться звонки — мобильное приложение партнёра упало, интеграция с CRM перестала работать, на сайте вместо имён пользователей — пустые строки. И всё это из-за одного поля.

Версионирование API — это не бюрократия и не усложнение ради усложнения. Это способ менять внутренности системы, не ломая тех, кто уже на неё завязался.

Почему это вообще проблема

Когда API публичный или им пользуются несколько команд, каждый потребитель пишет код под конкретную структуру ответов. Мобильное приложение ждёт массив items, внешний сервис — объект data.results, партнёрская интеграция — поле price в копейках.

Если ты меняешь контракт без предупреждения, ты ломаешь их код. Даже если твоё изменение «логичнее» или «правильнее» — это не имеет значения. Клиенты платят за стабильность, а не за твоё видение чистой архитектуры.

Проблема усугубляется тем, что у разных клиентов разный цикл обновлений. Веб-приложение можно обновить за час. Мобильное приложение сначала проходит ревью в App Store — от трёх дней до недели. Встроенное ПО на устройствах вообще может не обновляться годами. Ты не можешь просто сказать «обновитесь».

Три основных подхода к версионированию

Версия в URL

Самый распространённый и понятный способ:

GET /api/v1/users/42
GET /api/v2/users/42

Плюсы очевидны: версию видно в логах, в браузере, в документации. Легко тестировать — просто меняешь цифру в адресе. Можно держать несколько версий одновременно на разных роутах.

Минусы тоже есть. Технически это нарушает REST — URI должен указывать на ресурс, а не на версию API. Но на практике большинству это не мешает спать. GitHub, Stripe, Twilio — все используют именно этот подход.

Когда выбирать: когда API публичный, когда клиентов много и они разные, когда нужна максимальная прозрачность.

Версия в заголовке

GET /api/users/42
Accept: application/vnd.myapp.v2+json

Или кастомный заголовок:

GET /api/users/42
X-API-Version: 2

URL остаётся чистым, версионирование — в метаданных запроса. Теоретически правильнее с точки зрения REST.

На практике — неудобно. Нельзя просто открыть в браузере. Сложнее дебажить. Новички в команде клиента постоянно забывают добавить заголовок. Документация становится запутаннее.

Когда выбирать: если принципиально важна «чистота» URL и клиенты — опытные разработчики, которые точно разберутся.

Версия в query-параметре

GET /api/users/42?version=2

Простой вариант, часто встречается в старых API. Работает, но выглядит как временное решение. Версию легко не заметить или потерять при копировании URL.

Когда выбирать: для внутренних API или быстрых прототипов. Для продакшн-API с внешними клиентами — лучше выбрать что-то другое.

Что именно версионировать

Не каждое изменение требует новой версии. Есть обратно совместимые изменения — их можно делать без версионирования:

  • Добавление нового поля в ответ (клиенты, которые его не знают, просто игнорируют)
  • Добавление нового эндпоинта
  • Добавление нового необязательного параметра в запрос
  • Расширение допустимых значений поля (например, добавление нового статуса в enum)

А вот это — ломающие изменения, которые требуют новой версии:

  • Переименование поля
  • Удаление поля
  • Изменение типа поля (было число, стала строка)
  • Изменение структуры (было плоским, стало вложенным)
  • Изменение поведения существующего эндпоинта
  • Удаление эндпоинта
  • Изменение кодов ошибок

Простое правило: если старый клиентский код может перестать работать — это ломающее изменение.

Семантическое версионирование для API

SemVer (MAJOR.MINOR.PATCH) придуман для библиотек, но его логику можно применять и к API:

  • MAJOR — ломающие изменения. Именно эта цифра идёт в URL: v1, v2, v3
  • MINOR — новые возможности без поломок. Обычно не отражается в URL, но документируется
  • PATCH — багфиксы, не меняющие поведения

На практике большинство команд версионируют только major-версии в URL, а minor и patch просто документируют в changelog. Этого достаточно.

Как жить, пока две версии работают одновременно

Это самый болезненный момент. После релиза v2 ты не можешь сразу выключить v1 — клиенты ещё не мигрировали. И ты вынужден поддерживать обе.

Несколько практических советов:

Объяви дату выключения заранее. Минимум за три месяца до отключения v1 — уведомление в документации, по email, в заголовках ответов. Stripe при deprecation добавляет заголовок Sunset с датой:

Sunset: Sat, 01 Jan 2027 00:00:00 GMT

Логируй использование. Прежде чем выключать старую версию, убедись, что ей никто не пользуется. Метрики по версиям — обязательны. Иначе ты узнаешь о живых клиентах только после того, как они упадут.

Не держи зоопарк. Больше двух-трёх версий одновременно — уже боль. Поддержка трёх кодовых баз вместо одной, три набора тестов, три набора документации. Чем раньше ты обнуляешь старые версии — тем лучше.

Сделай миграцию дешёвой. Если переход с v1 на v2 требует переписать половину интеграции — клиенты будут тянуть до последнего. Хорошая документация изменений, migration guide с примерами кода, а в идеале — инструмент автоматической миграции.

Обратная совместимость как культура

Лучшее версионирование — то, которое не нужно. Если с самого начала думать об обратной совместимости, версии меняются редко.

Несколько паттернов, которые помогают:

Никогда не удаляй поля сразу. Сначала — deprecation (пометил как устаревшее), потом — через версию — удаление. Так у клиентов есть время адаптироваться.

Добавляй, не меняй. Вместо того чтобы переименовать user_name в username — добавь username рядом, оставь user_name. Потом в следующей мажорной версии убери старое поле.

Будь осторожен с enum. Если ты добавляешь новое значение в перечисление — убедись, что клиенты корректно обрабатывают неизвестные значения. Хороший клиентский код не падает на switch/case при виде незнакомого статуса.

Не меняй смысл поля. Это хуже переименования. Если поле amount раньше было в рублях, а ты решил перевести в копейки — это катастрофа. Лучше добавить новое поле amount_kopecks и задокументировать переход.

Документация и коммуникация

Версионирование без документации — бесполезно. Клиенты должны знать:

  • Какие версии существуют и что в них
  • Что изменилось между версиями (changelog с примерами)
  • Когда старые версии будут выключены
  • Как мигрировать

Changelog должен быть человеческим. Не «рефакторинг поля идентификатора» — а «поле id теперь возвращает UUID вместо числа, обновите парсинг на стороне клиента».

Если у тебя есть партнёры или enterprise-клиенты — уведомляй их лично, не надейся, что они сами прочитают документацию. Письмо с конкретными шагами и дедлайном работает лучше любого changelog.

Инструменты, которые помогают

Несколько вещей, которые облегчают жизнь:

OpenAPI / Swagger — описывай контракт API в машиночитаемом формате. Тогда можно автоматически находить ломающие изменения при сравнении двух версий спецификации. Инструменты вроде openapi-diff или breaking-change-detector делают это за минуты.

Consumer-driven contract testing — клиент описывает, какой контракт он ожидает, и этот контракт запускается в CI поставщика. Если ты сломал что-то, что нужно клиенту — тесты падают до того, как изменение ушло в прод. Pact — самый известный инструмент для этого.

API Gateway — если у тебя несколько версий, gateway помогает роутить запросы к нужной версии бэкенда, трансформировать запросы/ответы и собирать метрики по использованию версий.

Реальный пример: как это выглядит на практике

Допустим, в v1 ответ на запрос пользователя выглядит так:

{
  "id": 42,
  "user_name": "ivan_petrov",
  "full_name": "Иван Петров",
  "phone": "+79001234567"
}

В v2 ты хочешь добавить адрес, переименовать поля и вынести контакты в отдельный объект:

{
  "id": "user_42",
  "username": "ivan_petrov",
  "name": {
    "first": "Иван",
    "last": "Петров"
  },
  "contacts": {
    "phone": "+79001234567",
    "email": "ivan@example.com"
  },
  "address": {
    "city": "Москва",
    "street": "Ленина, 1"
  }
}

Изменений много — это точно новая мажорная версия. Что делаешь:

  1. Запускаешь /api/v2/users/42 с новой структурой
  2. Оставляешь /api/v1/users/42 работать без изменений
  3. Публикуешь migration guide: что изменилось, как поменять код
  4. Добавляешь в ответы v1 заголовок с датой выключения
  5. Через три месяца смотришь в метрики — если v1 никто не использует, выключаешь

Просто. Никакой магии.

Когда версионирование не нужно

Если API только внутренний и все потребители — твои команды, которые деплоятся вместе с API — строгое версионирование избыточно. Достаточно договориться о процессе: сначала мигрируем потребителей, потом меняем API.

Если ты на этапе активной разработки и API ещё не стабилен — можно явно пометить его как v0 или beta и предупредить, что контракт может меняться. Это честно и снимает ожидания.

Мы в REEXY при разработке API для клиентских проектов всегда закладываем версионирование с первого дня — даже если сейчас версия одна. Стоимость добавить /v1/ в роуты сейчас — ноль. Стоимость рефакторинга всех клиентов через год, когда понадобится v2 — значительно больше.

Главное, что стоит запомнить

Версионирование — это контракт с клиентами. Ты говоришь: «Пока версия та же — ничего не сломается». Это обещание, и его нужно держать.

Начни с версии в URL — это самый понятный подход. Заведи правило: ломающие изменения — только в новой мажорной версии. Логируй использование версий. Объявляй deprecation заранее и держи слово по срокам выключения.

Если нужна помощь с проектированием или разработкой API для вашего продукта — REEXY занимается интеграцией сервисов и разработкой с нуля, посмотреть подробности можно на r3xy.ru.