Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Версии API и совместимость

Версионирование API: как менять контракт, не ломая клиентов

Стратегии версий API: URI/заголовок, совместимые и ломающие изменения, deprecation-цикл, контрактное тестирование и правила эволюции публичного интерфейса.

Прямой ответ

Как только у API появляется внешний потребитель, любое изменение контракта — потенциально ломающее, и процесс важнее схемы нумерации. Рабочая рамка: различать совместимые и несовместимые изменения списком (добавить поле/эндпоинт/опциональный параметр — совместимо; удалить/переименовать поле, изменить тип/семантику, добавить обязательный параметр, сузить допустимые значения — ломает); совместимые — выкатывать без версий (клиенты с tolerant reader не замечают), несовместимые — через версию с deprecation-циклом (старое продолжает работать N месяцев, changelog + заголовок Deprecation/Sunset, миграция описана). Схема версий — вторична: URI (/v1/ — видимая, грубая), заголовок/параметр (API-Version: 2026-10-01 — дата-версии Stripe-стиля, тоньше и без «вечного v1 в URL»), главное — постоянство выбора и машинная читаемость контракта (OpenAPI как источник истины: диффы контракта проверяются в CI до мержа). И правило, которое экономит больше всего нервов: сужать и чистить публичный контракт заранее — каждое лишнее публичное поле станет обещанием, которое придётся держать годами.

Что ломает клиентов: классификация изменений

Совместимые (выкатываются тихо): новые эндпоинты и методы; новые опциональные поля запроса с дефолтом; новые поля ответа (клиент обязан игнорировать неизвестное — tolerant reader; клиенты, падающие на неизвестном поле, — их баг, но встретится обязательно); расширение enum значений ответа (клиентский switch без default сломается — предупреждать в changelog всё равно); ослабление валидации (принимать больше, чем принимали). Ломающие: удаление/переименование полей и эндпоинтов; смена типа (string→number), формата даты, структуры (объект→массив); новый обязательный параметр/заголовок; сужение диапазона/enum; изменение семантики при той же форме (статус 200 стал 201; пагинация сменила смысл курсора; ошибка стала другой при тех же условиях); изменение поведения аутентификации. Серая зона — «исправление бага, на которое кто-то полагался» (решается по обстоятельствам: заметное изменение поведения — как ломающее, с deprecation, даже если «по спецификации было правильно»). Классификация фиксируется в гайдлайнах команды (дизайн API — раздел эволюции) — споры «ломает/не ломает» решаются по документу, не по настроению ревьюера.

Схемы версий: URI, заголовок, дата

URI-версии (/v1/orders, /v2/orders) — самая распространённая: видна в логах/браузере/доках, роутится тривиально (прокси/фреймворк), минус — крупные версии поощряют «копить ломающее до v2 годами» и плодят параллельные линии поддержки. Версия заголовком/медиа-типом (API-Version: 2 или Accept: application/vnd.api+json;version=2) — URL чистый, версионирование — свойство запроса; требует дисциплины (версия по умолчанию для запросов без заголовка фиксируется навсегда). Дата-версии Stripe-модели (Stripe-Version: 2026-10-01): каждая учётка закреплена за датой-версией, изменения применяются к новым ключам, старые видят свой контракт — максимально плавная эволюция без «больших взрывов v2»; цена — внутренняя сложность (слой преобразования ответов под версию клиента — оправдано для платных публичных API с долгим хвостом интеграций). Что выбрать: внутренний API/мало потребителей — URI или заголовок с редкими мажорами; публичный продуктовый API — дата-версии или строгая URI-политика с длинным deprecation; GraphQL — отдельная модель (схема одна, эволюция через deprecation-директивы полей, версий нет вовсе).

Deprecation-цикл и контрактные тесты

Процесс ломающего изменения: (1) решение и запись в план (что, почему, чем заменяется); (2) публикация нового рядом со старым (новое поле/эндпоинт — старое помечено deprecated в OpenAPI + заголовки Deprecation: true/Sunset: <дата> в ответах); (3) уведомление потребителей (changelog, письмо интеграциям — webhook-подписчики узнают первыми); (4) окно миграции (месяцы; телеметрия использования старого — по метрикам видно, кто ещё ходит (мониторинг): напоминания адресные, не «всем когда-нибудь»); (5) удаление после даты Sunset (или продление с явным решением — молчаливое «передвинули и забыли» хуже исходной проблемы). Контрактное тестирование ловит непреднамеренные поломки до деплоя: OpenAPI-дифф в CI (breaking-изменение в диффе без явной пометки версии — красная сборка — инструменты oasdiff-класса), consumer-driven контракты (Pact: тесты потребителей прогоняются против провайдера — «наш рефакторинг сломал чужой клиент» узнаётся в PR, не в проде), смоук-набор реальных запросов против новой версии. Версионность артефактов: OpenAPI-спеки хранятся по версиям (доки v1 живут, пока жив v1), semver спеки растёт с изменениями.

Практический сценарий: изменение расчёта цены без инцидента

Задача: API заказов должен вернуть цену с новой схемой скидок (старое поле total включает доставку, новое разбиение — subtotal/discount/shipping/total_new). Анализ: менять смысл total — ломающее (все клиенты пересчитают отчёты молча — худший вид поломки: без ошибок, с неверными числами). План: (1) additive-релиз: новые поля добавлены, total не тронут (совместимо — выкатывается без версии); (2) deprecation: total помечен deprecated в OpenAPI, заголовок Deprecation в ответах, статья миграции с примерами (diff старого/нового расчёта); (3) телеметрия: метрика чтения полей (кто использует total — 140 интеграций) — рассылка по контактам + напоминание через 3 месяца (осталось 40); (4) срок Sunset +6 месяцев: за месяц — точечная работа с оставшимися (крупные — созвоны, хвост — письма); (5) v2 (заголовок-версия): total отсутствует, контракт чище; v1 живёт ещё полгода для отставших и закрывается по Sunset. Итог: ноль инцидентов у потребителей, старая схема ушла по плану; контрпример из практики коллег (сменили формат даты «это же багфикс») — три сломанные интеграции и горячая неделя: классификация изменений как процесс окупается именно в таких мелочах.

FAQ

Нужны ли версии внутреннему API одной команды? Полные — нет (потребитель рядом, ломается и чинится в одном деплое — фиче-флаги и контракты в OpenAPI достаточны); дисциплина классификации изменений полезна и там (внутренних потребителей обычно больше, чем кажется, — «внутренний» API обрастает скриптами); версии — когда появляются другие команды с независимым циклом деплоя.

Что делать, если ломающее изменение нужно срочно (безопасность)? Безопасность — легитимное исключение из deprecation-цикла (уязвимое поведение убирается немедленно, потребители ломаются с объяснением и быстрым путём миграции); коммуникация и постмортем обязательны — «срочно и молча» превращает инцидент безопасности в инцидент доверия.

Как версионировать вебхуки и события? Версия схемы в payload события (event_version: 2) + подписка на явные версии (потребитель обновляется сам, в своём темпе); изменение схемы — новая версия события, старая шлётся до Sunset подписки; «шлём всем одну схему и надеемся» — повторяет все проблемы неверсионированного API с задержкой доставки.

Сколько версий API поддерживать одновременно? Сколько обещали политика (обычно N-1/N-2 для публичных); каждая живая версия — тесты, доки, слой совместимости (стоимость растёт линейно) — аргумент в пользу редких мажоров и долгого deprecation вместо частых vN; метрика — доля трафика по версиям: хвост <5% на старой версии — кандидат на закрытие по плану.

Источники: OpenAPI, Stripe API versioning, GraphQL best practices — ссылки в шапке.