Обратная совместимость и deprecation
Урок о том, как развивать API годами, не ломая клиентов, которых вы не контролируете.
Обратная совместимость — это свойство нового релиза API, при котором клиенты, написанные под старую версию, продолжают работать без изменений.
Версионирование (урок 1) — это тяжёлая артиллерия: новая мажорная версия стоит дорого и вам, и клиентам. В идеале к ней прибегают редко, а большинство изменений вкатывают совместимо, вообще без новой версии. Чтобы так уметь, нужно чётко понимать, какие изменения безопасны, а какие — нет, и как корректно выводить из эксплуатации то, что всё же приходится ломать.
Что ломает совместимость
Ломающим считается любое изменение, после которого корректный старый клиент начинает работать неправильно. Типичные виновники:
- Удаление поля из ответа — клиент, который его читал, получает
undefined/null. - Переименование поля (
name→full_name) — для клиента это удаление одного и добавление другого. - Сужение типа — было число или строка, стало только число; был объект с тремя ключами, стало с двумя.
- Превращение необязательного в обязательное — старые запросы без этого поля начинают отклоняться.
- Изменение семантики при том же имени — поле
statusраньше было"ok"/"fail", стало числом. Самый коварный случай: схема как будто та же, а смысл другой. - Ужесточение валидации — раньше принимали, теперь
400.
Что безопасно: аддитивные изменения
Совместимы изменения, которые добавляют, не трогая существующего:
- Новое поле в ответе (клиент его не ждёт и игнорирует).
- Новый необязательный параметр запроса со значением по умолчанию.
- Новый эндпоинт или новый метод на существующем ресурсе.
- Новое значение в перечислении — условно безопасно (см. толерантного читателя ниже).
Правило-памятка:
| Совместимо (аддитивно) | Ломает |
| добавить поле в ответ | удалить/переименовать поле |
| добавить необязательный параметр | сделать параметр обязательным |
| добавить эндпоинт | сузить тип значения |
| расширить enum (осторожно) | сменить смысл существующего поля |
Толерантный читатель (tolerant reader)
Половина дела — на стороне сервера, половина — на стороне клиента. Принцип толерантного читателя гласит: клиент должен читать только то, что ему нужно, и игнорировать всё остальное. Не падать на незнакомых полях, не валить ответ из-за нового значения в enum, не требовать строгого порядка ключей. Это прямое следствие принципа надёжности Постела: «будь строг к тому, что отправляешь, и терпим к тому, что принимаешь».
Сервер прислал:
{ "id": 7, "name": "Аня", "badge": "new" } <- поле badge добавили
Хрупкий клиент: падает, потому что схема "не совпала".
Толерантный клиент: читает id и name, badge просто игнорирует.
Толерантный читатель — причина, по которой добавление поля считается совместимым: оно ломает только тех клиентов, которые написаны плохо. Документируйте этот контракт явно: «мы можем добавлять поля в любой момент, не считая это ломающим изменением».
Процесс deprecation
Рано или поздно что-то всё же придётся убрать. Делается это не одномоментно, а через объявленный процесс устаревания. HTTP даёт для этого стандартные заголовки. Deprecation помечает, что эндпоинт/версия устарели, а Sunset называет дату, после которой они перестанут работать:
HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Link: <https://api.example.com/docs/migration-v2>; rel="deprecation"
curl -i https://api.example.com/v1/users/42
# в ответе видим заголовки Deprecation и Sunset
Полный процесс вывода из эксплуатации обычно такой:
- Объявить. Запись в changelog, заголовки
Deprecation/Sunset, письмо известным потребителям. - Дать миграционный период. Старое и новое работают параллельно — недели или месяцы, чтобы клиенты успели переехать.
- Предупреждать перед концом. Напоминания по мере приближения даты
Sunset. - Убрать. Только после
Sunsetи желательно с понятной ошибкой (410 Goneсо ссылкой на новую версию), а не молчаливым404.
Как работает под капотом
Заголовки Deprecation/Sunset — это просто метаданные ответа; сервер их выставляет на устаревших маршрутах, а зрелые клиенты читают и логируют, поднимая алерт, что пора мигрировать. Часто этого мало: API-провайдеры строят телеметрию использования — считают, кто из клиентов ещё дёргает устаревший эндпоинт, чтобы адресно достучаться до отстающих перед отключением. Миграционный период технически держится на сосуществовании: старый и новый код живут рядом, нередко старый превращается в тонкий адаптер поверх новой реализации. А changelog — это формальный контракт коммуникации: каждое заметное изменение фиксируется с датой и пометкой совместимости, чтобы у клиента был единый источник правды.
Частые ошибки
- Удалять без предупреждения. Молча выпиленное поле или эндпоинт — самый быстрый способ сломать прод клиентам и потерять доверие.
- Менять смысл, сохраняя имя. Опаснее явного слома: тесты на схему проходят, а поведение поехало. Лучше новое имя/поле.
- Слишком короткий миграционный период. Внешним клиентам нужны недели и месяцы, не дни. Учитывайте релизные циклы партнёров.
- Нет changelog. Без письменной истории изменений клиент не знает, что и когда поменялось, и не доверяет API.
- Хрупкий клиент. Жёсткий парсинг, падающий на незнакомом поле, превращает безопасные аддитивные изменения в ломающие. Пишите толерантных читателей.
Итоги
- Ломают совместимость удаление, переименование, сужение типа, ужесточение требований и смена семантики; добавление — безопасно.
- Стремитесь вкатывать изменения аддитивно, оставляя новую мажорную версию на крайний случай.
- Толерантный читатель: клиент игнорирует незнакомое — это делает добавление поля совместимым.
- Устаревание — это процесс: объявить (
Deprecation/Sunset, changelog) → миграционный период → предупреждения → удаление с понятной ошибкой. - Никогда не ломайте молча и не меняйте смысл поля под старым именем.