коротко

API живёт долго, а его клиенты — мобильные приложения, партнёрские интеграции — обновляются не сразу и не все. Поэтому API нельзя менять как попало. Ломающее изменение (breaking change) — то, что обрушит существующих клиентов: удалить или переименовать поле, сделать необязательное поле обязательным, сузить тип. Неломающее — то, что старые клиенты переживут: добавить необязательное поле, новый эндпоинт. Безопасный путь — менять API аддитивно (только добавлять) и приучить клиентов к толерантному чтению (игнорировать незнакомые поля). Когда без слома никак — выпускают новую версию (часто /v2/ в URL) и дают старой срок жизни перед отключением (депрекация).

Однажды я согласовал «маленькую правку»: переименовать в ответе API поле sum в более понятное amount. На бэкенде это пять минут. Выкатили — и через час посыпались жалобы из мобильного приложения: у части пользователей в чеках вместо суммы пусто. Оказалось, старая версия приложения, которая стоит у людей в телефонах, ищет именно поле sum, а его больше нет. Обновление в сторе вышло, но дошло не до всех — кто-то не обновляется месяцами. Та «правка на пять минут» сломала тысячи живых клиентов. С тех пор я знаю: у API нет кнопки «обновить всех сразу».

Зачем вообще версионировать

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

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

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

Ломающее и неломающее изменение

Граница между «можно менять спокойно» и «сломаешь клиентов» проходит по одному вопросу: переживёт ли это старый клиент, который ничего не знает о вашем изменении?

Неломающее (аддитивное) изменение старый клиент не замечает. Вы добавили в ответ новое поле — клиент, который про него не знает, просто его не читает, и всё работает как раньше. Вы добавили новый эндпоинт — старые клиенты в него не ходят, им всё равно.

Ломающее изменение выдёргивает у клиента то, на что он рассчитывал. Убрали поле, которое он читает, — он получает пусто. Переименовали — то же самое. Сделали раньше необязательное поле обязательным — старый клиент его не шлёт и получает ошибку. Сузили тип (было «строка любой длины», стало «не больше 20 символов») — то, что раньше проходило, теперь отвергается.

ИзменениеЛомающее?Почему
Добавить необязательное поле в ответНетСтарый клиент его просто игнорирует
Добавить новый эндпоинтНетСтарые клиенты в него не ходят
Добавить необязательный параметр запросаНетБез него поведение прежнее
Удалить или переименовать полеДаКлиент читает то, чего больше нет
Сделать необязательное поле обязательнымДаСтарый клиент его не присылает → ошибка
Сузить тип или диапазон значенийДаТо, что раньше проходило, теперь отвергается
Поменять смысл существующего поляДаКлиент трактует данные неверно, молча

Самое коварное — тихий слом

Поменять смысл поля, не меняя его имя и тип, — худший вид ломающего изменения. Было поле amount в рублях, стало в копейках. Технически ответ валиден, клиент не падает — он просто показывает сумму в сто раз больше, и никто не замечает, пока не приходит злой пользователь. Такие изменения не ловятся проверкой формата, их ловит только человек, который думает про контракт. Это ваша работа как аналитика.

Правило аддитивности и толерантное чтение

Из таблицы выше следует главный приём: меняйте API только добавлением. Нужно новое поле — добавьте его необязательным, не трогая старые. Нужен новый формат ответа — сделайте новый эндпоинт рядом, не ломая существующий. Пока вы только добавляете, вы не ломаете никого. Большинство изменений можно уместить в эту модель, если думать о ней заранее.

Вторая половина правила — на стороне клиента. Хорошо написанный клиент практикует толерантное чтение (tolerant reader): он берёт из ответа только те поля, которые ему нужны, и спокойно игнорирует все незнакомые. Тогда сервер может свободно добавлять поля, и ни один клиент от этого не сломается.

Запишите это в требования к клиенту

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

Способы версионирования и их цена

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

Версия в URL — самый частый и наглядный: /v1/orders и /v2/orders живут рядом как разные адреса.

GET /v1/orders/123     — старый контракт, старые клиенты
GET /v2/orders/123     — новый контракт, новые клиенты

Плюс — видно сразу, версию легко указать и протестировать, её видно в логах. Минус — формально это не очень «честный» REST: один и тот же заказ 123 получает два разных адреса. На практике этим минусом почти все пренебрегают ради наглядности.

Версия в заголовке — клиент шлёт что-то вроде Accept: application/vnd.api.v2+json, а URL остаётся один. Плюс — адрес ресурса не двоится. Минус — версию не видно в адресе, её легко забыть указать, сложнее тестировать руками и труднее объяснить партнёрам.

Версия через тип данных — разновидность предыдущего, когда версия зашита в указании формата (тот самый vnd...v2). Те же плюсы и минусы: чище академически, неудобнее на практике.

Что выбрать на практике

Для публичного API и партнёров почти всегда берут версию в URL — потому что её видно, её легко продиктовать в документации и проверить через браузер или curl. Заголовки и типы выбирают команды, которым важна академическая чистота REST и которые контролируют обе стороны. Если сомневаетесь — /v1/ в URL, это скучный, понятный и рабочий выбор.

Депрекация: как убивать старую версию

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

flowchart LR
  A["v1 живёт одна"] --> B["вышла v2; v1 помечена deprecated"]
  B --> C["оба работают параллельно; клиенты переезжают"]
  C --> D["дата отключения настала; v1 выключают"]

Схема показывает жизненный цикл версии. Сначала есть только v1. Потом выходит v2, а v1 объявляется устаревшей (deprecated) — но продолжает работать. Какое-то время — недели, месяцы, для крупных API иногда год и больше — обе версии живут параллельно, и клиенты постепенно переезжают на v2. И только когда объявленная дата отключения настала (а старая версия почти опустела), v1 наконец выключают. Главное: между «объявили устаревшей» и «выключили» всегда есть запас времени, и он тем больше, чем меньше вы контролируете клиентов.

Частые вопросы

Что такое breaking change (ломающее изменение)?

Это изменение API, которое обрушивает существующих клиентов, не знающих об этом изменении. Типичные примеры: удалить или переименовать поле в ответе, сделать раньше необязательное поле обязательным, сузить тип или диапазон значений, а также тихо поменять смысл поля (например, рубли на копейки). Противоположность — неломающее, аддитивное изменение: добавить необязательное поле или новый эндпоинт, которые старые клиенты просто не замечают.

Как версионировать API?

Сначала старайтесь обходиться без версий, меняя API только аддитивно — добавляя необязательные поля и новые эндпоинты, не трогая старые. Когда слом неизбежен, выпустите новую версию параллельно старой: самый наглядный способ — версия в URL (/v1/, /v2/), есть также вариант с версией в заголовке. Старую версию не выключайте сразу — пометьте устаревшей (депрекация), назовите дату отключения и дайте клиентам срок переехать, тем больший, чем меньше вы их контролируете.

Что такое обратная совместимость?

Обратная совместимость означает, что новые версии сервера продолжают корректно работать со старыми клиентами, которые ещё не обновились. Достигается двумя приёмами: сервер меняет API только аддитивно (добавляет, не удаляет и не переименовывает), а клиент практикует толерантное чтение — берёт нужные поля и игнорирует незнакомые. Это критично, потому что клиенты вроде мобильных приложений и партнёрских интеграций обновляются не сразу и не все.