Изменения API

Лучшие практики версионирования API, ради вызывающих

6 мин чтения

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

Когда следует версионировать API?

Версионируйте API только когда изменение сломало бы корректного вызывающего. Аддитивные изменения, новые поля, новые endpoint, новые опциональные параметры не нуждаются в версии; вызывающие, написанные под старый контракт, продолжают работать, а новая возможность просто существует. Breaking change нуждается в ней, потому что альтернатива — узнать об этом вызывающему из ошибки. Версионирование каждого релиза, включая аддитивные, учит вызывающих, что версии — это шум, и они перестают читать важные уведомления.

Практический тест такой же, как в статье о breaking change: если вызывающий, полагавшийся только на документированное поведение, должен что-то изменить, чтобы продолжить работать, изменению нужна версия. Если нет, выпустите его под текущей версией и напишите запись changelog.

Какую схему версионирования API следует использовать?

Используйте схему, которую ваши вызывающие могут легче всего видеть и устанавливать, что для большинства публичных API — это версия в пути URL или датированный заголовок версии. Четыре общие схемы различаются меньше в возможностях, чем в том, что они требуют от вызывающего, и это правильная основа для выбора.

СхемаПримерЧто должен сделать вызывающийКто использует
Путь URL/v2/invoicesИзменить URL при миграцииБольшинство публичных REST API
Заголовок версииX-GitHub-Api-Version: 2022-11-28Отправить заголовок, или принять умолчаниеGitHub
Датированная версия аккаунтаStripe-Version: 2026-08-26Закрепить дату на запрос или на аккаунтStripe
Параметр запроса/invoices?version=2Добавить параметрБолее старые API; редко выбирается сейчас
Тип медиаAccept: application/vnd.example.v2+jsonСогласовывать типы содержимогоПуристы; мало вызывающих справляются

Путь URL самый заметный и наименее гибкий. Каждый вызывающий может видеть, в какой он версии, читая строку лога, а скачок версии — это найти-и-заменить. Цена: вся поверхность двигается сразу, вы не можете изменить контракт одного endpoint без выпуска новой версии для всех, поэтому версии пути, как правило, редки и велики.

Заголовок версии держит URL стабильными и позволяет серверу выбрать умолчание для вызывающих, ничего не отправляющих, так работает версионирование REST API GitHub: версия, названная по дате, в X-GitHub-Api-Version, с самой старой поддерживаемой версией как умолчанием, чтобы неверсионированные вызывающие не ломались. Цена: версия невидима в URL и легко забывается в новом клиенте.

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

Параметр запроса и тип медиа оба работают и оба проваливают тест видимости по-разному: параметр запроса легко теряется при построении URL, а версия типа медиа невидима почти для любого инструмента, которым вызывающий бы отлаживал. Схема Stripe с датами является самым известным примером подхода с датой, и как Stripe версионирует свой API разбирает её подробно.

Как версионирование API делается на практике?

На практике версия — это именованный набор поведений, и сервер отображает каждый запрос на одно из них. Шаги одинаковы независимо от того, какая схема несёт имя.

  1. Называйте версии по дате или целому числу, не по семантической версии. Веб-API — это не пакет. Вызывающие не могут закрепить minor версию URL, поэтому v2 или 2026-08-26 говорит всё, что нужно вызывающему, а семантическое версионирование подразумевает обещание совместимости, которое схема не может выполнить.
  2. Держите версию вне путей кода, которым она безразлична. Версия должна выбирать слой трансляции на границе, а не разветвлять бизнес-логику. Две полные копии кодовой базы — это как версия оказывается неподдерживаемой.
  3. Давайте каждой версии умолчание и документ. Вызывающие, не отправляющие версию, получают самую старую поддерживаемую, никогда не самую новую, чтобы незакреплённый клиент не сломался в день релиза. У каждой версии есть страница, говорящая, что изменилось по сравнению с предыдущей.
  4. Установите окно поддержки и опубликуйте его. Руководство Google по версионированию, AIP-185, требует разумного и заранее объявленного переходного периода и рекомендует 180 дней даже для бета-функциональности. Выберите окно, запишите его, и применяйте без пересогласования по каждой версии.
  5. Выводите версии из эксплуатации так же, как выводите endpoint. Версия, прошедшая своё окно, получает то же обращение, что и любой депрекированный API: объявление, заголовок Sunset (RFC 8594) в каждом ответе, напоминание на полпути оставшимся вызывающим, и дата удаления, которая соблюдается.

Что такое v1 и v2 в REST API?

v1 и v2 — это имена для двух контрактов, которые один и тот же сервер поддерживает одновременно. v2 существует, потому что что-то в v1 нельзя было изменить без разрушения его вызывающих, поэтому изменение пошло в новый контракт, а старый продолжил работать. Номера не подразумевают, что v2 завершён или что v1 мёртв; оба верны, только если документация так говорит. v3, появляющийся каждый квартал, — это признак того, что версионируются аддитивные изменения, или что контракт никогда не проектировался для поглощения изменений.

Это модель версионирования через путь URL, где номер версии — сегмент, который набирает вызывающий. Сервисы gRPC обычно решают ту же проблему иначе: версия живёт в имени пакета внутри самого файла .proto. gRPC и Protobuf разбирает эту разницу и то, почему совместимость на проводе там определяется номерами полей, а не формой URL.

Что должно объявлять изменение версии?

Изменение версии должно объявлять, что ломается, кого затрагивает, как мигрировать, и как долго предыдущая версия продолжает работать. У записи та же форма, что у любой другой записи об изменении, ломающем совместимость, плюс строка, объявляющая окно поддержки. Вот одна для API, версионированного заголовком:

Версия API 2026-11-01 доступна. Версия 2025-06-15 поддерживается до 1 ноября 2027 года. Новое в 2026-11-01: GET /invoices возвращает amount в минимальных единицах как целое число вместо десятичной строки, а депрекированное поле customer_name удаляется в пользу объекта customer. Затрагивает вызывающих на 2025-06-15, парсящих amount как строку, что является умолчанием для незакреплённых клиентов, созданных до июня 2025 года. Миграция: парсите amount как целое число и читайте имя из customer.name. Закрепите X-Api-Version: 2026-11-01, когда будете готовы. Ничего не меняется для вызывающих, не закрепляющих версию.

Последнее предложение — то, что позволяет большинству читательниц перестать читать, и оно принадлежит каждому объявлению версии. Страница примеры changelog включает записи от API, версионирующих таким образом, и разница между хорошими и остальными в основном в этом последнем предложении.

Кого уведомляют, когда версия меняется?

Всех на старой версии, индивидуально, и changelog для всех остальных. Изменение версии — это единственный случай, когда «мы опубликовали об этом что-то» гарантированно пропускает именно тех вызывающих, которые важны: тех, кто закрепил версию два года назад и с тех пор не читал заметки о релизе. Данные об использовании отвечают, кто они; уведомление должно достичь их там, где их код, в заголовках ответа и в сообщении владелице аккаунта.

В цикле, который мы выполняем, запись, объявляющая версию, составляется из pull request, который её выпускает, рецензируется человеком, и публикуется на ленте и виджете, где версионированный клиент может прочитать её как JSON. Каждый, чей отзыв из виджета просил об изменении или сообщал о баге, который оно решает, и стал GitHub issue, который закрывает этот pull request, уведомляется в этом issue, как только запись выходит в эфир. Механизм тот же, что и для любой записи; скачок версии — это просто запись с самой высокой ставкой.

FAQ

Должно ли каждое изменение API получать новую версию? Нет. Только изменения, ломающие совместимость. Аддитивные изменения выпускаются под текущей версией с записью changelog. Версионирование аддитивных изменений учит вызывающих игнорировать версии.

Лучше ли версионирование через URL, чем через заголовок? Версионирование через URL легче видеть вызывающим и сложнее вам развивать постепенно; версионирование через заголовок наоборот. Для публичного API с множеством мелких клиентов версионирование через URL проваливается реже. Для крупного API со слоем трансляции датированная версия через заголовок масштабируется лучше.

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

Что должны получать невероенные запросы? Самую старую поддерживаемую версию, чтобы существующие незакреплённые клиенты продолжали работать, с заголовком ответа, сообщающим им, какую версию они получили.


Технические утверждения в этой статье не проходили независимую проверку. Если здесь что-то не так, сообщите нам, и мы исправим.

По теме на changeloop: Документация для разработчиков, Примеры changelog

changeloop
Команда, которая делает changelog, замыкающий цикл. Пользователи о чём-то просят, ваша команда это делает, тот, кто просил, узнаёт об этом.