API
OpenAPI: сделать контракт источником истины, а не доками
Бэкендер добавляет поле tenantId в payload создания заказа и помечает его required — изменение в одну строку, выкатил в пятницу. К понедельнику три мобильных клиента, партнёрская интеграция и внутренняя админка возвращают 422 на каждый заказ. Никто из них не слал tenantId; никто не знал, что оно есть. Контракт говорил, что поле опционально вчера и обязательно сегодня, и ничто в пайплайне этого не заметило. «Доки» были страницей в Confluence, отредактированной полгода назад. Настоящий контракт жил у кого-то в голове и поменялся, никому не сказав.
Спека — это контракт, иначе фикция
OpenAPI — это машиночитаемое описание твоего HTTP API: каждый путь, форма каждого запроса и ответа, каждый статус-код, каждая схема авторизации, записанные в YAML или JSON. Столько у большинства команд есть. Вопрос, который решает, помогает оно или вредит, лежит на уровень глубже: спека — это источник истины или описание истины, записанное где-то ещё?
Когда спека — побочный документ (написали один раз, выгрузили на портал доков, никогда не сверяли с работающим сервисом), она гниёт за месяцы. Код меняется, спека — нет, и теперь у тебя два контракта: тот, что сервер обеспечивает, и тот, что заявляет спека. Клиенты интегрируются по спеке, спека — фикция, и интеграция падает в проде. Это contract drift, и это исход по умолчанию, пока что-то активно ему не мешает.
Фикс — не «писать доки лучше». Фикс — сделать спеку несущей: код генерируется из неё, запросы валидируются по ней, а её дифф гейтит каждый pull request. Контракт, который нельзя нарушить без падения билда, — это контракт. Всё остальное — страница в вики.
Spec-first vs code-first: где живёт истина
Получить спеку можно двумя способами, и разница в том, какой артефакт авторитетен.
Spec-first (design-first): ты пишешь OpenAPI-документ руками, ревьюишь его как дизайн API и генерируешь серверные заглушки, типизированных клиентов и моки из него. Спека — источник истины; код ей соответствует. Фронтенд может строить против сгенерированного мока в тот же день, когда бэкенд только начал, потому что контракт существует раньше любой реализации.
Code-first: ты пишешь сервер с аннотациями или декораторами (@ApiProperty, type hints в FastAPI, springdoc), а инструмент генерирует спеку из работающего кода. Код — источник истины; спека — его отражение. Меньше церемоний, и спека не разойдётся с хендлером, из которого её извлекли, — но может разойтись с тем, чего ждали потребители, потому что никто не отревьюил контракт как контракт до выкатки.
Ни то ни другое не ошибка. Решение сеньора такое: spec-first, когда API потребляют несколько команд или внешние партнёры и контракт нужно ревьюить и держать стабильным; code-first, когда одна команда владеет обоими концами и скорость важнее предварительного дизайна. Что не опционально в любой модели — это enforcement: спека должна валидироваться и диффиться, как бы она ни родилась.
| Измерение | Spec-first | Code-first |
|---|---|---|
| Источник истины | Написанная руками спека | Код сервера + аннотации |
| Контракт ревьюят до кода? | Да — это дизайн-артефакт | Нет — рождается из реализации |
| Параллельная работа клиент/сервер | С первого дня (мок из спеки) | После компиляции сервера |
| Риск drift | Спека vs реализация (валидируй на edge) | Контракт vs ожидания потребителя |
| Лучше всего для | Публичные API, много потребителей | Одна команда владеет обоими концами |
Что реально даёт OpenAPI 3.1
Спека отрабатывает не самим документом, а тулингом, который открывает. Важны четыре выигрыша:
- Сгенерированные типизированные клиенты и серверные заглушки. Прогони генератор по спеке и получи SDK на TypeScript, Go, Swift, Kotlin — с методами, моделями, типами ошибок. Класс интеграционных багов («я слал
userId, серверу нуженuser_id») исчезает, потому что запрос больше никто не пишет руками. Команды, выкатывающие публичные API, генерируют SDK из спеки именно чтобы убить эту категорию целиком. - Валидация запросов на edge. Middleware валидирует входящие запросы по спеке до того, как они дойдут до хендлера. Битое тело отклоняется точным 400 с именем виноватого поля, а твоя бизнес-логика мусора не видит.
- Доки, которые не разойдутся молча. Когда отрендеренные доки генерируются из той же спеки, что гейтит CI, «доки врут» перестаёт быть возможным — доки и есть контракт.
- Мок-серверы. Инструмент отдаёт фейковые ответы прямо из примеров спеки, и потребители интегрируются раньше, чем появился бэкенд.
Конкретно OpenAPI 3.1 выравнивает язык схем с JSON Schema 2020-12, так что твои ключевые слова валидации — это настоящий JSON Schema, а не подмножество эпохи 3.0. Цена этой мощи: nullable: true больше нет — теперь пишешь type: ["string", "null"] — а exclusiveMinimum/exclusiveMaximum принимают число, а не булево. 3.1 ещё добавляет верхнеуровневый webhooks для документирования событий, которые твой API шлёт, а не только принимает. Многие инструменты по-прежнему дефолтят в 3.0, так что проверь поддержку генератора до апгрейда.
Почему это работает
Генераторы кода не магия — они кодируют мнения. Спека с рыхлыми схемами (additionalProperties: true, без массивов required, нетипизированные object-блобы) генерирует рыхлых, нашпигованных any клиентов, что убивает смысл. Дисциплина, которая делает генерацию ценной, — это плотные схемы: явные типы, явный required, переиспользуемые компоненты. Генератор честен ровно настолько, насколько честен контракт, который ты ему скармливаешь.
Детект ломающих изменений: гейт, который ловит пятничный деплой
У истории с tenantId один чистый фикс: инструмент, который диффит новую спеку против предыдущей версии и роняет PR на ломающем изменении. oasdiff — частый выбор: он классифицирует 450+ категорий изменений и знает, какие ломающие. Добавление required-поля запроса, удаление поля ответа, которое клиент может читать, сужение типа, удаление значения enum, удаление эндпоинта — всё ломающее, всё ловится до мержа.
Вшитый в CI, чек туп и эффективен: пайплайн диффит спеку из PR против main, и если появляется ломающее изменение, билд красный, а инженер видит ровно какое поле сломалось — раньше любого клиента. Пятничный деплой не доходит до прода, потому что контракт обеспечивается тем же гейтом, что и тесты. Тонкость в 3.1: так как nullability переехала в массив типов, удаление "null" из type: ["string", "null"] регистрируется как смена типа, а не как смена nullable — дифф всё равно ловит это, просто по другому правилу, поэтому смотри на категорию, которую сообщает инструмент, а не только на количество.
Публичный API потребляют мобильные приложения, которые ты не можешь принудительно обновить, и три партнёрские интеграции. Выбери стратегию enforcement.
PR добавляет новое required-поле в тело запроса. Что должен сделать здоровый API-пайплайн?
Твои написанные руками доки говорят, что поле опционально, но сервер теперь отклоняет запросы без него. Как это называется и что это предотвращает?
Расставь шаги spec-first пайплайна, который предотвращает drift:
- 1 Написать / обновить OpenAPI-спеку и отревьюить её как контракт
- 2 CI диффит спеку против предыдущей версии (oasdiff) и роняет билд на ломающем изменении
- 3 Сгенерировать типизированных клиентов, серверные заглушки и мок из спеки
- 4 Валидировать входящие запросы по спеке на edge до того, как отработает хендлер
- 5 Рендерить доки из той же спеки, чтобы они не могли противоречить контракту
- 01Коллега говорит: «у нас уже есть OpenAPI-спека, значит, мы защищены от ломающих изменений». Почему это не так и чего на самом деле не хватает?
- 02Когда выбрать code-first вместо spec-first, и какой enforcement всё равно нужен в любом случае?
OpenAPI — это машиночитаемое описание HTTP API, но описание бесполезно, пока оно не источник истины. Сбойный режим — contract drift: написанные руками доки говорят одно, сервер обеспечивает другое, клиенты интегрируются против фикции, и изменение в одну строку вроде добавления required-поля ломает каждого потребителя в проде. Spec-first считает написанную руками спеку авторитетной и генерирует из неё клиентов, заглушки и моки; code-first генерирует спеку из аннотаций. В любом случае спеку надо обеспечивать — сгенерированные типизированные клиенты убивают целый класс багов с запросами вручную, валидация на edge отклоняет битый ввод до хендлера, а дифф ломающих изменений (oasdiff) роняет PR в тот момент, когда кто-то удаляет поле, сужает тип или добавляет required. OpenAPI 3.1 выравнивает схемы с JSON Schema 2020-12, выкидывает nullable ради массивов type и добавляет webhooks. Уплотни схемы переиспользуемыми $ref-компонентами, явным required и схемами авторизации, а затем дай пайплайну сделать контракт невозможным сломать молча.