DarkRiDDeR15 мин

Code review API-изменения: от diff до обратимой миграции

Code reviewМиграции

Проблема в code review API-изменения редко выглядит как красная строка. Автор добавляет обязательное поле, меняет enum или удаляет старый response-property, а reviewer видит только локально зелёные тесты. Цена ошибки появляется после публикации: разные версии клиента начинают спорить с одним сервером, а быстрый rollback уже не возвращает удалённое поле.

Причина — просматривать diff как изменение одного репозитория. API имеет потребителей, кэш, документацию, генераторы типов и иногда асинхронные события. Поэтому проверка должна начинаться с классификации изменения, продолжаться поиском потребителей и заканчиваться обратимой последовательностью. Ниже — практический маршрут, который можно применить к одному pull request.

Сначала классификация, потом обсуждение кода

У каждой строки схемы есть направление совместимости. Добавление необязательного response-поля обычно расширяет контракт. Удаление поля сужает его. Добавление обязательного поля в request ломает старого отправителя, а изменение response-типа ломает десериализацию даже при том же имени. Эта классификация не заменяет review, но не даёт обсуждать все изменения одинаково.

Функция classifyApiChange ниже намеренно принимает уже выделенные факты diff. Она не пытается сама прочитать OpenAPI и не делает вывод о конкретной команде. Это удобная граница для теста: если генератор diff ошибся, его ошибка находится до классификатора; если классификатор выбрал breaking, reviewer получает повод проверить совместимость.

Минимальная карта API-diff
ВопросПризнакЧто проверитьРешение
Старый клиент отправит запрос?Новое required request-полеВсе builders и fixturesDefault, optional или новая версия
Старый клиент прочитает ответ?Удаление/переименование поляПоиск доступа к propertyDeprecated-период и новое поле
Старое значение остаётся допустимым?Сужение enumВетвления клиентов и событийРасширить enum или сменить версию
Сохранилась семантика?Тот же тип, другое значениеДокументация и consumer testЯвно описать смысл и миграцию
Можно вернуть сервер?Изменение хранения или записиBackward read и rollbackСначала expand, затем switch, потом contract

Обратимость начинается с данных

Откат бинарного файла не откатывает базу и сообщения в очереди. Если новый сервер записал только новый формат, старый сервер может не суметь прочитать данные. Поэтому для опасного API-изменения полезен expand/contract: сначала добавить совместимое поле или колонку, затем научить код читать и писать оба формата, переключить потребителей и только после подтверждения удалить старую форму.

На review стоит попросить не обещание «rollback возможен», а конкретную матрицу. Какие версии читают старую запись? Как выглядит запись после частичного переключения? Что произойдёт с повторной доставкой события? Где хранится сигнал, что старый consumer ещё жив? Ответы превращают риск в проверяемые условия, а не в уверенность по названию ветки.

Маршрут review API-изменения: diff проходит через классификацию совместимости, проверку потребителей и окно обратимой миграции перед удалением старой формы.
Диаграмма связывает локальный diff с потребителями и данными. Красная ветка означает остановку до удаления, если старый формат ещё нужен.

Runnable-пример: классифицируем diff

Вход функции — объект с тремя признаками: удалённые свойства, новые обязательные свойства и сужение enum. На выходе — breaking или compatible и действие для review. Это не автоматическое разрешение pull request. Пример полезен как первая страховка, после которой нужны реальные consumer tests и проверка данных.

import { classifyApiChange } from './upgrade-2027-09.mjs';

const additive = classifyApiChange({
  removedProperties: [],
  addedRequiredProperties: [],
  narrowedEnum: false,
});
const risky = classifyApiChange({
  removedProperties: ['displayName'],
  addedRequiredProperties: [],
  narrowedEnum: false,
});

console.log(additive.status, additive.action);
console.log(risky.status, risky.action);
// compatible run-consumer-contract-tests
// breaking version-or-expand-compatibility-window

Порядок review для одного diff

  1. Скопируйте в описание изменения старую и новую форму запроса, ответа и события. Diff схемы без примеров заставляет reviewer восстанавливать смысл по именам.
  2. Запустите классификатор и вручную проверьте каждый breaking-признак: удаление, required, enum, тип и изменение семантики.
  3. Найдите потребителей по сгенерированным типам, сериализаторам, документации и тестовым fixture. Отдельно проверьте неизвестные внешние клиенты.
  4. Составьте матрицу чтения и записи старой и новой формы. Укажите, что произойдёт при частичном rollout и повторной доставке события.
  5. Добавьте отрицательные contract-тесты для старого клиента и положительные для нового. Тест должен падать на конкретном поле, а не на общем статусе.
  6. Опишите условие удаления старой формы: сигнал использования, срок хранения и способ восстановления. Без этого «временное поле» становится вечным.

Ограничения автоматической классификации

Классификатор не знает, что displayName обязателен для внешнего клиента, а внутренний клиент его игнорирует. Он не проверяет кэш, подписанные payload, очереди и генерацию SDK. Даже правильный статус breaking не говорит, как долго держать две версии. Это инструмент сортировки риска, не замена архитектурному решению.

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

Ограничения и следующий шаг

Статья не описывает конкретный CI, брокер или схему базы. Примеры синтетические и запускаются локально; они показывают форму решений, а не результат изменения внешнего API. Для опасных контрактов потребуется интеграция с registry схем, consumer tests и наблюдаемым сигналом использования старого поля.

Следующий шаг — выбрать один настоящий diff и заполнить четыре артефакта: старая/новая схема, таблица потребителей, тест частичного rollout и процедура удаления. Если хотя бы один потребитель неизвестен, оставьте расширение совместимым и не переходите к contract-фазе миграции.

Проверяемые источники

  • OpenAPI Specification 3.1.1 — OpenAPI Initiative, 24 октября 2024 года, версия 3.1.1. Применение: Фиксирует структуру HTTP-интерфейса, операции, ответы и семантику описания, чтобы контракт был машинно читаемым. Граница: Не доказывает, что сервер действительно отдаёт описанное тело: runtime-проверка и тесты остаются отдельной обязанностью.
  • JSON Schema Core 2020-12 — JSON Schema, draft 2020-12, спецификация Core. Применение: Задаёт язык типов, обязательных полей, ограничений и ветвления для JSON-документов. Граница: Схема не знает бизнес-состояние, права доступа, задержку или согласованность нескольких запросов.
  • RFC 9110 — HTTP Semantics — IETF, июнь 2022 года, RFC 9110, Standards Track. Применение: Разделяет метод, статус, представление ресурса и условия обмена, на которые опирается совместимость. Граница: Не описывает локальную реализацию сервиса, формат внутренней базы или конкретный клиент.