Совместимое изменение API
Каталог развивается: появляются новые свойства курса, методы и способы выдачи. Клиенты обновляются в разное время, поэтому изменение сервера встречается не только с новейшим интерфейсом. Совместимость означает, что прежний допустимый сценарий продолжает работать с тем же обещанным смыслом, а не просто получает какой-нибудь успешный статус.
В этом уроке определим границы расширения нашего API. Результат — вы сможете отличить добавление возможностей от скрытого разрыва договора и выбрать отдельную версию, когда она нужна. Никакие маршруты сейчас не перемещаются. Сценарии сравниваются по тексту контракта, без исполнения клиентов.
Начинаем с прежнего допустимого клиента
До изменения публичный курс содержит четыре поля:
{"id":"c001","title":"Современный JavaScript","topic":"frontend","lessons":40}
Клиент читает id, title, topic и lessons, а неизвестные дополнительные поля игнорирует. Такое поведение было частью договорённости о чтении. Сервер может добавить необязательное summary, сохраняя прежние значения и типы; старому клиенту всё ещё достаточно знакомых данных.
{"id":"c001","title":"Современный JavaScript","topic":"frontend","lessons":40,"summary":"Основы языка для веб-разработки"}
Второй объект является возможным будущим расширением, а не заменой исходных данных во всех наших уроках. Он показывает, при каком клиентском правиле добавление совместимо. Если конкретный старый клиент отвергает любые незнакомые свойства вопреки договору, практическая миграция всё равно потребует внимания, но ошибка клиента не делает первоначальное обещание иным.
При этом вход POST/PUT остаётся строгим: сервер запрещает неизвестные поля. Открытый ответ не означает, что можно переслать весь полученный объект назад как вход. Новое summary потребует отдельного определения редактируемости, нормализации и права на изменение, прежде чем клиент станет его отправлять.
Обязательность меняет прежний запрос
Допустим, сервер решил потребовать difficulty в каждом PUT. Прежний клиент отправляет только title, topic и lessons и теперь получает 422. Это несовместимое изменение, хотя само новое поле выглядит полезным и старые три поля никуда не исчезли.
Можно сначала добавить необязательный вход с определённым значением по умолчанию. Но и default требует проверки смысла: он не должен неожиданно менять видимость или безопасность прежнего курса. Если невозможно честно выбрать прежний эквивалент, разумнее вводить новый договор с явной миграцией.
Аналогично удаление поля, изменение string на object или lessons из числа в строку ломает старое чтение. Преобразование 40 в "40" не становится совместимым лишь потому, что JavaScript иногда умеет привести тип. Другой клиент и строгая схема имеют право ожидать прежнее число.
Значение поля важнее формы JSON
Сохраним lessons числом, но начнём считать в нём минуты обучения. Синтаксис не изменился, однако клиент покажет пользователю ложное число уроков. Это такой же разрыв смысла, как смена типа. Единицы, диапазон, порядок и правило нормализации относятся к договору.
Сортировка списка тоже способна изменить сценарий. Если прежний default был lessons по убыванию и id по возрастанию, его замена сортировкой title изменит страницы и продолжение чтения. Даже если клиент не передавал sort, он использовал опубликованное значение по умолчанию.
Порядок query-параметров в URL не должен становиться случайной бизнес-семантикой. Но сами допустимые параметры, обработка повторов и неизвестных имён должны оставаться согласованными. Удаление прежнего фильтра или новое скрытое ограничение может разорвать интеграцию без изменения схемы Course.
Новое значение enum требует проверки
Наш topic принимает frontend, backend, quality и tools. Добавление security как нового ответа выглядит расширением набора, однако клиент мог исчерпывающе обработать четыре варианта. При новом значении его ветка отображения или типовая модель станет неполной.
known topics: frontend | backend | quality | tools
future value: security
question: is an unknown-value fallback part of the client contract?
Если договор заранее предусматривал запасную обработку неизвестного значения, расширение проще. Если enum объявлен закрытым, нельзя без обсуждения отправить пятый вариант в прежней версии. Открытость дополнительных полей и открытость значений уже существующего поля являются разными обещаниями.
Новый статус задания имеет похожую проблему. Добавить paused между running и succeeded недостаточно: старый клиент может бесконечно ждать или считать его failed. Нужно определить, как он обращается с неизвестным state, либо изолировать новое состояние в новом договоре.
Версии разных объектов нельзя смешивать
OpenAPI info.version обозначает версию описания API. ETag обозначает выбранное представление конкретного ресурса. revision события обозначает последовательность изменений курса, а snapshot s1 — фиксированную выборку для пагинации. Совпадение цифр у этих значений не означает общую шкалу.
Если курс c001 изменился и получил новый ETag, это не обязательно выпуск API v2. Если добавлена совместимая документационная возможность, клиенту не нужно менять курсор текущего снимка. И наоборот, новая несовместимая семантика API не разрешает игнорировать проверку версии ресурса при PUT.
Курсор — непрозрачный договор продолжения. Его привязка к фильтру, порядку, limit и сроку десять минут сохраняется. Смена внутреннего формата может происходить без видимого изменения строки, но действующие старые курсоры должны либо оставаться пригодными до своего срока, либо получить заранее оговорённый отказ. Внезапная инвалидизация всех курсоров является наблюдаемым изменением.
URL можно сохранить при расширении
Опыт ProfessorWeb напоминал, насколько важны старые адреса. В API тоже полезно сохранить работающий путь, если его смысл остаётся прежним. Добавление отдельной операции не требует переименовать существующий GET, только чтобы название выглядело современнее.
Когда действительно нужен несовместимый договор, возможен отдельный префикс /api/v2. Это пример стратегии, а не новый работающий маршрут каталога. Прежний /api продолжает прежнюю семантику на время оговорённой миграции. Поддерживать два адреса недостаточно, если оба скрыто читают новую несовместимую форму ответа.
Для обсуждения версии полезна опубликованная спецификация, но OpenAPI не выбирает за автора правила совместимости. Структура версии документа OpenAPI 3.1.1. Редакционный номер спецификации сам по себе не делает смену полей безопасной.
Повторы должны пережить миграцию осмысленно
Область нашего Idempotency-Key включает subject, метод и путь. Если клиент после потери ответа повторит старое намерение на /api/v2 с независимым реестром, оно способно стать новой операцией. Нельзя объявить поддержку ключей и забыть историю принятия в старой версии.
План миграции должен дать способ разрешить прежний неопределённый исход: прочитать старый результат или использовать явно согласованную общую идентичность операции. Автоматическое объединение ключей разных версий тоже опасно, если нормализация и входной смысл различаются. Один и тот же JSON может означать разные бизнес-данные.
Для webhook изменения требуется отдельно согласовать форму события, правила подписи и окно ротации ключа. Получатели могут хранить старые события и обрабатывать их позже. Новый JSON не должен задним числом менять смысл уже подписанных байтов, а новая signature version не может появиться без поддерживающего перехода.
Права и кеш также являются частью поведения
Если прежде публичный GET становится персональным, общий кеш больше не подходит. Нужно менять границу доступа и Cache-Control, а не только добавить поле owner. Если уменьшается срок сессии или доступ Alice к frontend, интерфейс должен разбирать новые отказы; безопасность может требовать изменения, но оно всё равно наблюдаемо для клиента.
При совместимом дополнительном поле сильный ETag JSON меняется, потому что байты представления изменились. Это ожидаемый новый валидатор, а не нарушение API. При этом HTML может иметь свой отдельный тег. 304 допустим только при подходящем условии соответствующего представления; старый тег нельзя сохранить из желания «не ломать кеш».
Перед будущим выпуском полезно выбрать несколько прежних допустимых сценариев и пройти их по новому тексту: чтение, PUT, курсор, повтор POST и закрытый результат. Здесь такое сопоставление редакционное, а не запуск тестов. Последний урок соберёт эти границы в единый разбор, чтобы новый API не выглядел согласованным только на счастливом одиночном запросе.