Проблема в code review API-изменения редко выглядит как красная строка. Автор добавляет обязательное поле, меняет enum или удаляет старый response-property, а reviewer видит только локально зелёные тесты. Цена ошибки появляется после публикации: разные версии клиента начинают спорить с одним сервером, а быстрый rollback уже не возвращает удалённое поле.
Причина — просматривать diff как изменение одного репозитория. API имеет потребителей, кэш, документацию, генераторы типов и иногда асинхронные события. Поэтому проверка должна начинаться с классификации изменения, продолжаться поиском потребителей и заканчиваться обратимой последовательностью. Ниже — практический маршрут, который можно применить к одному pull request.
Сначала классификация, потом обсуждение кода
У каждой строки схемы есть направление совместимости. Добавление необязательного response-поля обычно расширяет контракт. Удаление поля сужает его. Добавление обязательного поля в request ломает старого отправителя, а изменение response-типа ломает десериализацию даже при том же имени. Эта классификация не заменяет review, но не даёт обсуждать все изменения одинаково.
Функция classifyApiChange ниже намеренно принимает уже выделенные факты diff. Она не пытается сама прочитать OpenAPI и не делает вывод о конкретной команде. Это удобная граница для теста: если генератор diff ошибся, его ошибка находится до классификатора; если классификатор выбрал breaking, reviewer получает повод проверить совместимость.
| Вопрос | Признак | Что проверить | Решение |
|---|---|---|---|
| Старый клиент отправит запрос? | Новое required request-поле | Все builders и fixtures | Default, optional или новая версия |
| Старый клиент прочитает ответ? | Удаление/переименование поля | Поиск доступа к property | Deprecated-период и новое поле |
| Старое значение остаётся допустимым? | Сужение enum | Ветвления клиентов и событий | Расширить enum или сменить версию |
| Сохранилась семантика? | Тот же тип, другое значение | Документация и consumer test | Явно описать смысл и миграцию |
| Можно вернуть сервер? | Изменение хранения или записи | Backward read и rollback | Сначала expand, затем switch, потом contract |
Обратимость начинается с данных
Откат бинарного файла не откатывает базу и сообщения в очереди. Если новый сервер записал только новый формат, старый сервер может не суметь прочитать данные. Поэтому для опасного API-изменения полезен expand/contract: сначала добавить совместимое поле или колонку, затем научить код читать и писать оба формата, переключить потребителей и только после подтверждения удалить старую форму.
На review стоит попросить не обещание «rollback возможен», а конкретную матрицу. Какие версии читают старую запись? Как выглядит запись после частичного переключения? Что произойдёт с повторной доставкой события? Где хранится сигнал, что старый consumer ещё жив? Ответы превращают риск в проверяемые условия, а не в уверенность по названию ветки.
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
- Скопируйте в описание изменения старую и новую форму запроса, ответа и события. Diff схемы без примеров заставляет reviewer восстанавливать смысл по именам.
- Запустите классификатор и вручную проверьте каждый breaking-признак: удаление, required, enum, тип и изменение семантики.
- Найдите потребителей по сгенерированным типам, сериализаторам, документации и тестовым fixture. Отдельно проверьте неизвестные внешние клиенты.
- Составьте матрицу чтения и записи старой и новой формы. Укажите, что произойдёт при частичном rollout и повторной доставке события.
- Добавьте отрицательные contract-тесты для старого клиента и положительные для нового. Тест должен падать на конкретном поле, а не на общем статусе.
- Опишите условие удаления старой формы: сигнал использования, срок хранения и способ восстановления. Без этого «временное поле» становится вечным.
Ограничения автоматической классификации
Классификатор не знает, что 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. Применение: Разделяет метод, статус, представление ресурса и условия обмена, на которые опирается совместимость. Граница: Не описывает локальную реализацию сервиса, формат внутренней базы или конкретный клиент.