Версионирование API и контрактная совместимость
TL;DR
Совместимость — это дисциплина, а не удача. Держите четкую политику версий (SemVer), математику изменений (что «ломает», что нет), контрактные тесты, регистры схем и Sunset-процедуры. Для денег и комплаенса — строгий REST/gRPC с vN, для UI-агрегаций — эволюционный GraphQL с `@deprecated`. Всегда: канареечный трафик, обратная совместимость ≥ один релизный цикл, миграционные гайды, телеметрия использования полей.
1) Базовые понятия и цели
Backwards-compatible (BC): новому серверу подходят старые клиенты.
Forwards-compatible (FC): старому серверу подходят новые клиенты (ограниченно).
Wire-совместимость: формат на «проволоке» не ломается (особенно важно для gRPC/Protobuf).
SemVer: `MAJOR.MINOR.PATCH` — ломаете контракт → повышаете MAJOR.
Цель: минимизировать ломающие изменения и обеспечивать предсказуемые окна миграции.
2) Матрица изменений: что можно, а что нельзя
3) Политики для разных стилей API
3.1 REST
Версия в URI (`/v1/...`) или в домене (`api-v1.`). Заголовочная версия — только для внутренних случаев.
Добавляйте, не удаляйте: новые поля — ок, старые — помечайте как `deprecated` в схеме и оставляйте минимум на один цикл.
Статусы/ошибки: не меняйте коды и структуру `error.code/error.message/error.details`.
Идемпотентность неизменна: не превращайте безопасный `POST` с `Idempotency-Key` в «поведенчески другой» вызов.
3.2 gRPC / Protobuf
Номера полей — священные: не переиспользуйте удаленные номера, помечайте как `reserved`.
Только добавление новых optional/репит полей; «жесткие обязательные» — через валидацию, не `required`.
Версионные пакеты: `payments.v1`, `payments.v2`.
Совместимость сервисов: новые RPC → новый метод; поведение старых не меняем.
proto message Payout {
reserved 4 ;//field deleted, number reserved string id = 1;
string currency = 2;
int64 amount_minor = 3;
// v2: optional string comment = 5;
}
3.3 GraphQL
Эволюция без v2: добавляйте поля/типы; удаление — через `@deprecated(reason)` с анонсом окна.
Persisted Queries: для публичных клиентов используйте белый список запросов — легче контролировать совместимость.
Field-level authZ и телеметрия: знайте, какие поля реально используются, прежде чем удалять.
graphql type Payout {
id: ID!
amountMinor: Long!
currency: String!
comment: String @deprecated(reason: "Use note")
note: String
}
3.4 Вебхуки
Версия в пути (`/webhooks/v1/payments`) и фиксированный конверт события (`event_id`,`type`,`ts`,`data`).
Подписи/HMAC сохраните неизменными; новые алгоритмы — как опция с флагом.
Расширения — только через новые поля `data.` и `headers` — без удаления старых.
4) API Gateway и маршрутизация версий
Rules-based routing: по префиксу `/v1`, по заголовку `X-Api-Version`, по домену.
Shadow/Canary: отражайте часть продакшн-трафика на новую версию «в тень», сравнивайте ответы.
Rate/Quotas per-version: защищает старые клиенты во время миграции.
- `Sunset:
` — дата выключения версии - `Deprecation: true` — версия устаревает
- `Link:
; rel="deprecation"` — на changelog/гайд миграции
nginx location ~ ^/v2/ {
proxy_pass http://api_v2;
}
location ~ ^/v1/ {
add_header Deprecation "true";
add_header Sunset "Thu, 01 May 2026 00:00:00 GMT";
proxy_pass http://api_v1;
}
5) Регистры схем и контракты
OpenAPI / JSON Schema для REST; Protobuf descriptors для gRPC; SDL registry для GraphQL.
CI-проверки: linters + «breaking-changes check» при PR.
Consumer-Driven Contracts (CDC): тесты потребителей (Pact/аналог) — защита от незаметных ломаний.
Changelog: machine-readable (например, `CHANGELOG.md` + релиз-ноты в реестре).
6) Эволюция полей: практические правила
ID/ключи: не меняйте формат (UUID↔int) без нового поля `_v2` и переходного периода.
Время/валюта: держите UTC ISO-8601/epoch и amount_minor + currency; не меняйте масштаб (копейки/центы).
Enum: добавляйте значения — ок; не меняйте смысл старых. Для REST — отдавайте string-значения, а не ints.
Пагинация: cursor-based стабильнее; не меняйте семантику курсора.
7) Деприкация и «Sunset»-процедура
1. Анонс (T-90/60): changelog, рассылка партнерам, заголовки `Deprecation/Sunset`.
2. Дублирующий период: V1 и V2 работают параллельно; V1 снабжен предупреждениями в ответах/логах.
3. Телеметрия использования: кто еще зовет V1? точечные контакты.
4. Заморозка V1: только багфиксы/без фич.
5. Выключение (Sunset): 410 Gone или блок-страница с инструкцией миграции.
8) Релизы без боли: стратегии выкладки
Blue/Green или Weighted routing: 1–5–25–50–100% трафика.
Compatibility window: минимум 1–2 минорных релиза, чаще 6–12 месяцев для внешних API.
Feature Flags: чтобы включать новые поля/ветки логики без смены версии.
Read/Write-расщепление: сначала добавьте поддержку чтения нового поля, затем начните писать его.
9) Тестирование совместимости (пакет практик)
Golden-тесты на ответы старых версий.
Diff-тесты схем: запрет breaking в CI.
Replay продакшн-трасс на staging для V2 (shadow).
Back/Forward сценарии: новый клиент на старом сервере и наоборот (там, где FC допустим).
Контрактные тесты вебхуков: проверка подписи, формата, времени.
10) Метрики и SLO процесса версионирования
% клиентов на последней MINOR (цель ≥ 80% перед Sunset).
Ошибки совместимости/недоступности на выпуск (цель — 0).
Доля вызовов устаревшей версии (убывающая к Sunset).
Время миграции клиента (медиана/п95).
Latency/regression delta между версиями (не хуже базовой).
11) Примеры артефактов
OpenAPI (фрагмент, деприкация поля):yaml components:
schemas:
Payout:
type: object properties:
id: { type: string, format: uuid }
amount_minor: { type: integer }
currency: { type: string }
comment:
type: string deprecated: true description: "Use note"
note: { type: string }
Protobuf (reserved и v2 пакет):
proto syntax = "proto3";
package payouts. v1;
message Payout { reserved 5; string id=1; int64 amount_minor=2; string currency=3; }
GraphQL (деприкация):
graphql type Query { payout(id: ID!): Payout }
12) Версионирование смежных каналов
SDK/CLI: SemVer + зависимость от API-версии, совместимость оговорена в README.
События/стримы (WS/Kafka): версия в `envelope.version`; новые атрибуты — опциональны; дедуп и резюм работают одинаково между версиями.
Отчетность/CSV: версия в имени файла/шапке; добавляйте столбцы справа; не меняйте порядок/типы.
13) Governance и роли
Владелец контракта (domain owner), API Steward (правила и линтеры), Release Manager (Sunset/коммуникации).
RFC-процесс для breaking-изменений: бизнес-обоснование, план миграции, артефакты, даты.
Единый каталог API: где видны схемы, версии, Sunset-календарь, контакт.
14) Анти-паттерны
«Тихие» ломания: меняем статус/ошибку/тип поля без версии.
Переиспользование protobuf-номеров — разрушает реплей и старые клиенты.
GraphQL без телеметрии использования полей — удаление «наощупь».
Глобальная v2 всего — мегамиграция вместо точечной эволюции.
Версия в query-параметре для публичного API — неочевидная и уязвимая схема.
Нет миграционных гайдов и примеров — партнеры буксуют, сроки срываются.
15) Check-list релиза новой версии
- Обновлена схема (OpenAPI/Protobuf/SDL), пройдены линтеры и breaking-checks.
- Добавлены интеграционные и контрактные тесты (CDC).
- Готовы SDK/пример кода/миграционный гайд и Changelog.
- Включены `Deprecation/Sunset` (для старой версии) + страница «How to migrate».
- Canary/Shadow план, алерты и дашборды сравнения метрик.
- Обратная совместимость сохранена на согласованный срок.
- План отката (rollback) и матрица рисков согласованы.
Резюме
Стабильное API — это процесс, а не «раз и навсегда». Живите по правилам: SemVer + add-only эволюция + регистр схем + контрактные тесты + Sunset-процедуры. Разделяйте стили (REST/gRPC/GraphQL) и их политики, маршрутизируйте версии на API Gateway, выкатывайте канарейками, измеряйте эффект. Так вы избежите «ломающих сюрпризов», ускорите интеграции партнеров и сохраните предсказуемость для денежных и комплаенс-критичных доменов.