DarkRiDDeR14 мин

Контракт до особого случая: как не превратить платформенный API в меню скрытых параметров

APIИнженерная практика

Проблема начинается не с поломки, а с вежливой просьбы: нестандартному потребителю нужен «ещё один флаг», сырой ответ или порядок, который раньше никто не называл. Если команда отвечает скрытым параметром, API получает вторую, неописанную поверхность. Цена — следующий потребитель начинает зависеть от случайного поведения, а изменение внутренностей превращается в расследование: это баг, обязательство или чей-то локальный обход?

Полезнее не спорить, достаточно ли случай особый. Сначала назвать контракт: операция, допустимый запрос, обязательный ответ, список ошибок, гарантия и граница расширения. Затем проверить конкретного именованного потребителя против этого списка. Действие короткое: если потребность нельзя объяснить через поверхность, создайте documented escape hatch с версией и пределом или остановите hand-off.

Абстракция протекает там, где нет названной границы

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

Контракт полезно читать как список разрешённых ожиданий, а не как снимок реализации. У операции есть имя и версия. У запроса — требуемые и допустимые поля. У ответа — обязательные поля, известные необязательные поля и правило, можно ли расширять форму. У ошибки — различимые состояния. У гарантии — только то, что команда готова повторять в пределах версии. Всё остальное остаётся деталью, даже если сегодня его легко наблюдать.

Вертикальная схема контрактной поверхности: сигнал потребителя проходит через операцию, запрос, ответ, ошибки и гарантию; documented escape hatch имеет имя и границу, а скрытый обход отмечен красным стопом.
Поверхность не обязана быть большой. Её задача — сделать видимым, что именно потребитель вправе ожидать, а что ещё не стало обязательством.
Минимальная контрактная поверхность
ЧастьВопрос к автору APIПример fixed reviewЧто не следует додумывать
операциякакое действие названо?readFixedRecordвнутренний способ чтения
запроскакой вход обязателен?recordIdскрытые параметры отладки
ответчто потребитель может читать?id и stateполе legacyMode отсутствует
ошибкикакое исключение различимо?fixed-not-foundполный список внутренних причин
гарантиякакой смысл обещан?state is a fixed symbolпорядок, скорость и будущие поля
escape hatchчто выдано сверх поверхности?raw-envelope-v1 с границейнеограниченный доступ к wire shape

Начать не с типа, а с решения потребителя

Тип ответа отвечает на вопрос «какие байты или поля возможны». Контракт отвечает на другой вопрос: «какое решение потребитель вправе принять на их основе». Для одного fixed reader достаточно различать id и state. Если другой reader требует legacyMode, это не аргумент тихо добавить поле в обещание. Сначала нужно выяснить, является ли это тем же семейством контрактов, сохранилось ли поле в целевой версии и может ли потребитель назвать условие миграции.

Такой порядок защищает и от чрезмерной универсальности. Не надо заранее превращать ответ в бесконечный объект «на всякий случай». Необязательное поле без правила расширения часто хуже отсутствующего поля: один потребитель не замечает его, другой делает из него обязательное условие, третий копирует его в собственную схему. Небольшая явная поверхность дешевле потому, что будущий спор имеет объект: можно сравнить предложение с объявленным контрактом, а не с памятью участников.

Исполняемый обзор поверхности

import { createFixedPlatformApiReview, reviewFixedPlatformApi, summarizeFixedContractSurface } from './upgrade-2026-01.mjs';

const review = createFixedPlatformApiReview('documented-compatible-v1');
const report = reviewFixedPlatformApi(review);
const surface = summarizeFixedContractSurface(review);
console.log({ status: report.status, fields: surface.responseFields, hatch: surface.documentedEscapeHatches });
// { status: 'synthetic-contract-review-hand-off', fields: ['id', 'state', 'label'], hatch: ['raw-envelope-v1'] }

Этот код импортирует только public exports и работает с named fixed literal. Он не отправляет request, не открывает сеть и не меняет API. Принятый status означает лишь, что в учебном объекте названы поверхность, версия, consumer и граница hatch. Именно поэтому output заканчивается hand-off, а не «обновить контракт»: пакет не получает права менять реальную схему, выпуск или сервис.

Escape hatch — отдельный договор, а не пароль для своих

Escape hatch нужен, когда общий контракт честно не покрывает задачу, но потребность всё же ограничена и проверяема. Хороший hatch имеет имя, версию, разрешённый вход, форму результата и отрицательную границу. В fixed example raw-envelope-v1 возвращает только одно named representation. Он не обещает порядок, фильтрацию, задержку, хранение, доступность или сохранение будущих полей. Такая граница может показаться сухой, но она не даёт одному наблюдению стать пятнадцатью неявными гарантиями.

Скрытый debug-wire опаснее не потому, что слово debug запрещено. У него нет документации и даже собственного предела. Значит, reviewer не знает, можно ли потребителю строить на нём парсер, повторять его после версии или передавать дальше. В данном fixture это fail-closed: незадокументированный hatch возвращает stop, хотя все остальные поля похожи на успешный review. Это полезный сигнал: сначала оформите отдельный договор либо уберите зависимость, не компенсируйте неопределённость красивым названием.

Короткая последовательность перед hand-off

  1. Записать решение. Назвать, какое действие должен выполнить именно этот потребитель, а не какой внутренний объект он хочет увидеть.
  2. Собрать поверхность. Зафиксировать operation, version, request, required response, errors и только проверяемые guarantees.
  3. Найти утечку. Отметить поле, порядок, исключение или режим, которого нет в поверхности, но без которого потребитель не работает.
  4. Выбрать форму. Либо расширить публичный контракт с правилами совместимости, либо оформить узкий documented escape hatch, либо вернуть stop.
  5. Сравнить consumer. Проверить family, version и требуемые response fields; результатом может быть только synthetic review hand-off.

Почему номер версии не заменяет этот разговор

Semantic Versioning полезен после того, как объявлен public API: он связывает несовместимое изменение с major version, а совместимое добавление — с minor version. Но номер не говорит, что именно public. Если скрытый параметр никогда не был назван поверхностью, один потребитель может считать его контрактом, а другой — случайностью. Сначала требуется конкретная запись обязательств; потом уже возможно обсуждать, какой номер соответствует их изменению.

OpenAPI решает ещё более раннюю часть задачи: даёт форму описания HTTP API, по которой человек или инструмент может увидеть способности без чтения исходного кода. Из этого не следует, что спецификация равна поведению. Документ может быть точным, неполным или устаревшим относительно реализации. Поэтому рядом с описанием нужен contract review: какой consumer сравнивался, какой факт проверялся и какое следующее действие разрешено. В этом пакете все эти объекты synthetic, так что вывод не распространяется на чужие документы.

Наблюдение потребителя ещё не является новым обязательством

Особый consumer часто приносит правильное наблюдение и слишком широкий вывод. Он может честно сказать: «мне сейчас нужен raw envelope, иначе я не вижу дополнительный marker». Из этого не следует, что каждый consumer должен увидеть весь envelope или что marker стабилен между версиями. В contract card нужно разделить три предложения: что увидел consumer, какое решение он не может принять без этого факта и какой минимальный интерфейс достаточен. Пока второе или третье предложение не записано, нельзя понять, нужно ли расширение public surface или локальный adapter.

Эта разница снижает стоимость дизайна. Вместо двух крайностей — добавить всё в основной response или отказать без объяснения — появляется третья: назвать узкое исключение и обеспечить его границу. В fixed fixture hatch не переносит гарантию state-is-a-fixed-symbol на raw форму и не добавляет обещание долговечности. Если новый reader захочет построить parser на безымянном поле, review должен остановиться раньше, чем этот parser станет внутренним стандартом команды. Контракт не запрещает потребность; он заставляет назвать её цену и владельца.

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

Эта практика не классифицирует все будущие изменения автоматически. Она не выбирает формат документа, не строит migration и не измеряет влияние на команду. Особенно важно не путать явный escape hatch с гарантией надёжности: у hatch есть ровно та граница, которая записана. Любая производительность, безопасность, долговечность или поведение при неизвестных полях требует отдельного контракта и отдельной проверки.

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

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

  • OpenAPI Specification v3.1.1 — версия: OpenAPI Specification 3.1.1, 24 October 2024, dated immutable publication. OAS 3.1.1 определяет language-agnostic interface description для HTTP API и описывает schema как описание request, response, parameter или header content. Граница: Спецификация не доказывает, что implementation ей соответствует, и не задаёт политику migration для synthetic API.
  • Semantic Versioning 2.0.0, exact source commit — версия: Semantic Versioning 2.0.0, commit 7c834b3f3a4940d77ab593bc32583004d6a426a9, 18 June 2013, immutable commit pin. Pinned SemVer 2.0.0 требует объявить public API и связывает incompatible public API change с major version. Граница: SemVer не определяет, какие поля данного fixed contract являются публичными, и не подтверждает совместимость consumer.
  • RFC 9110: HTTP Semantics — версия: RFC 9110, June 2022, immutable RFC publication. RFC 9110 описывает uniform interface и representation как передаваемую информацию о ресурсе, а не внутреннюю реализацию. Граница: RFC не задаёт application-level escape hatch, future-field policy или результат contract review.