Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

Совместимые контракты API и событий: меньше регрессий, быстрее релизы (JSON, Protobuf, Avro)

Разработка и технологии18 апреля 2026 г.
Контракт — это договор о формате данных между сервисами и командами. В статье — практики безопасной эволюции схем для REST/JSON, gRPC/Protobuf и Kafka/Avro, настройка совместимости через Schema Registry и контрактные тесты. Результат — меньше поломок интеграций, предсказуемые релизы и ускорение разработки без хаоса.
Совместимые контракты API и событий: меньше регрессий, быстрее релизы (JSON, Protobuf, Avro)

  • Содержание
    • Зачем бизнесу стабильные контракты
    • Типы совместимости: назад, вперёд, полная
    • REST/JSON и OpenAPI: правила эволюции
    • gRPC/Protobuf: как менять .proto без риска
    • Kafka/Avro + Schema Registry: строгий контроль
    • Процесс изменений и раскатки без простоев
    • Контрактные тесты (Pact): потребитель диктует
    • Инструменты и проверки в CI
    • Чек‑лист безопасной эволюции
    • Антипаттерны и грабли
    • Итоги

Зачем бизнесу стабильные контракты

Когда формат данных меняется непредсказуемо, ломаются интеграции, растут затраты на откаты и ручные правки. Стабильные контракты — это способ выпускать изменения быстро и безопасно:

  • меньше инцидентов и «невоспроизводимых» багов;
  • быстрый онбординг новых клиентов и партнёров;
  • предсказуемые сроки релизов и понятная зона ответственности между командами.

Контракт — это спецификация формата данных и допустимых изменений во времени. Он одинаково важен для REST/JSON, gRPC/Protobuf и потоков событий (Kafka/Avro).

Типы совместимости: назад, вперёд, полная

  • Назад совместимый (backward): новый продюсер (отправитель) понимается старым потребителем. Или, в терминах хранилища схем, новая схема совместима с данными, записанными по старой схеме.
  • Вперёд совместимый (forward): старый продюсер понимается новым потребителем.
  • Полная (full): одновременно назад и вперёд.

Для большинства продуктовых API и событий достаточно назад совместимости: мы можем добавлять поля, помечать как необязательные и заранее объявлять план вывода из эксплуатации (sunset) для старых полей.

REST/JSON и OpenAPI: правила эволюции

Базовые приёмы

  • Добавляйте только необязательные поля (optional). Старые клиенты их проигнорируют.
  • Не меняйте смысл существующих полей. Если смысл меняется — вводите новое поле, старое помечайте устаревшим и выводите по графику.
  • Не удаляйте поля внезапно. Сначала — депрекация, метки в документации, метрики использования, затем удаление.
  • Избегайте «ломающих» изменений в одном релизе: смена типов, форматов дат, переименование полей без совместимости — частая причина инцидентов.

OpenAPI: делаем несовместимое — невозможным

Поддерживайте спецификацию как код, проверяйте в CI. Пример: добавим необязательное поле loyaltyLevel для клиента.

openapi: 3.0.3
info:
  title: Customer API
  version: 1.3.0
paths:
  /customers/{id}:
    get:
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
components:
  schemas:
    Customer:
      type: object
      required: [id, email]
      properties:
        id: { type: string }
        email: { type: string, format: email }
        name: { type: string, nullable: true }
        loyaltyLevel:  # новое необязательное поле
          type: string
          enum: [basic, silver, gold]
          description: "Необязательное поле. Будет обязательным c 2026‑01‑01 — следите за объявлениями."

Проверяйте обратную совместимость спеки: линтеры (например, openapi-diff) детектируют опасные изменения — удаление полей, сужение enum, ужесточение ограничений.

gRPC/Protobuf: как менять .proto без риска

В Protobuf совместимость держится на номерах полей и внимательном отношении к типам.

Правила

  • Не меняйте номера полей. Номер — идентификатор на проводе.
  • Не меняйте тип на несовместимый (int32 -> string — плохо).
  • Поля можно добавлять как optional или внутри oneof.
  • Удалённые номера отмечайте как reserved, чтобы их никто не переиспользовал.
  • Для различения «пустое значение» и «поле отсутствует» используйте wrapper-типы (google.protobuf.*Value).

Пример эволюции

Версия v1:

syntax = "proto3";
package customer.v1;

message Customer {
  string id = 1;
  string email = 2;
  string name = 3; // может отсутствовать на стороне сервера
}

Версия v2: поле name выведем из эксплуатации, добавим loyalty_level и отметим зарезервированный номер.

syntax = "proto3";
package customer.v2;

import "google/protobuf/wrappers.proto";

message Customer {
  string id = 1;
  string email = 2;
  // 3 — резервируем, чтобы никто не переиспользовал старый номер
  reserved 3;
  // Новое необязательное поле с обёрткой — отличаем отсутствие от пустого
  google.protobuf.StringValue loyalty_level = 4; // "basic" | "silver" | "gold"
}

Если нужен «мягкий» переход, можно временно оставить имя как deprecated:

message CustomerV1Compat {
  string id = 1;
  string email = 2;
  string name = 3 [deprecated = true];
  google.protobuf.StringValue loyalty_level = 4;
}

Kafka/Avro + Schema Registry: строгий контроль

Avro-схемы хорошо подходят для событий и потоков: компактная сериализация и строгое определение совместимости через реестр схем.

Режимы совместимости

  • BACKWARD: новая схема читает старые данные.
  • FORWARD: старая схема читает новые данные.
  • FULL: и вперёд, и назад. Есть также транзитивные варианты (…_TRANSITIVE), гарантирующие совместимость со всеми прошлыми версиями, а не только с последней.

Пример: событие CustomerCreated

Схема v1:

{
  "type": "record",
  "namespace": "events.customer",
  "name": "CustomerCreated",
  "fields": [
    {"name": "id", "type": "string"},
    {"name": "email", "type": "string"}
  ]
}

Добавим необязательное поле loyaltyLevel с умолчанием — это назад совместимо.

{
  "type": "record",
  "namespace": "events.customer",
  "name": "CustomerCreated",
  "fields": [
    {"name": "id", "type": "string"},
    {"name": "email", "type": "string"},
    {"name": "loyaltyLevel", "type": ["null", {"type": "string", "default": "basic"}], "default": null}
  ]
}

Настройка Schema Registry через HTTP

Установим режим полной транзитивной совместимости для темы customers.created-value.

# Установить режим FULL_TRANSITIVE
curl -s -X PUT \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  --data '{"compatibility": "FULL_TRANSITIVE"}' \
  http://localhost:8081/config/customers.created-value

# Проверить совместимость новой схемы c последней версией
curl -s -X POST \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  --data '{"schema": "{\"type\":\"record\",\"namespace\":\"events.customer\",\"name\":\"CustomerCreated\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"loyaltyLevel\",\"type\":[\"null\",{\"type\":\"string\",\"default\":\"basic\"}],\"default\":null}]}"}' \
  http://localhost:8081/compatibility/subjects/customers.created-value/versions/latest

Если совместимость нарушена, реестр вернёт ошибку, а публикация схемы или запись сообщения будет отклонена в рантайме — это предотвращает инциденты.

Процесс изменений и раскатки без простоев

Думайте о контрактах как о миграциях, только для данных «на проводе».

Шаги безболезненной эволюции:

  1. Подготовка: определить цель, затронутые потребители, метрики использования старых полей/версий.
  2. Добавление: вносите новые необязательные поля/типы, включайте двоичную совместимость (Protobuf, Avro) и описывайте в спецификации (OpenAPI, .proto, Avro schema).
  3. Совместимость: временно поддерживайте оба варианта (старые и новые поля). На продюсере — можно публиковать и «старое», и «новое» поле (или маппить).
  4. Миграция потребителей: по очереди переводите потребителей на новую схему. Отслеживайте покрытие по метрикам.
  5. Депрекация: пометьте старые поля как устаревшие, включите алерты при их использовании.
  6. Удаление: после выдержки и согласованного окна — удаляйте «мёртвые» поля/эндпоинты. В Protobuf — mark reserved, в Avro — меняйте схему с проверкой.

Контрактные тесты (Pact): потребитель диктует

Когда команд много, «договор на словах» не работает. Потребительские контракты позволяют потребителю описать свои ожидания, а провайдеру — автоматически проверить, что он им соответствует.

Небольшой пример на JavaScript с использованием Pact, который проверяет, что поле email остаётся обязательным, а loyaltyLevel — опциональным.

// package.json должен содержать pact и jest
// npm i --save-dev @pact-foundation/pact jest
const path = require('path');
const { Pact } = require('@pact-foundation/pact');
const { Matchers } = require('@pact-foundation/pact');

const provider = new Pact({
  consumer: 'customer-web',
  provider: 'customer-api',
  dir: path.resolve(process.cwd(), 'pacts'),
  logLevel: 'warn'
});

describe('Customer API contract', () => {
  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());

  test('GET /customers/123 returns mandatory email and optional loyaltyLevel', async () => {
    await provider.addInteraction({
      state: 'customer 123 exists',
      uponReceiving: 'a request for customer 123',
      withRequest: { method: 'GET', path: '/customers/123' },
      willRespondWith: {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: Matchers.like('123'),
          email: Matchers.like('user@example.com'), // обязательно
          loyaltyLevel: Matchers.somethingLike('gold') // может отсутствовать
        }
      }
    });
    // В реальном тесте — вызов клиента и проверка ответа
    await provider.verify();
  });
});

В CI провайдер поднимает проверку контракта и не даёт сломать потребителей незаметно.

Инструменты и проверки в CI

  • Для OpenAPI: spectral, openapi-diff — проверка семантических изменений.
  • Для Protobuf: buf (lint + breaking-change detection), protoc-плагины со стилевыми правилами.
  • Для Avro/Kafka: Schema Registry с режимом FULL_TRANSITIVE и автоматической проверкой совместимости в CI перед деплоем продюсера.
  • Контрактные тесты: Pact Broker хранит контракты и матчит версии потребителей и провайдеров.
  • Политики в репозитории: обязательный «чек» совместимости при любом изменении .proto/.avsc/.yaml.

Пример шага CI для проверки Avro-схемы через Schema Registry:

#!/usr/bin/env bash
set -euo pipefail

SUBJECT="customers.created-value"
SCHEMA_FILE="schemas/customer_created_v2.avsc"
REGISTRY_URL="http://localhost:8081"

BODY=$(jq -n --arg schema "$(jq -c . < "$SCHEMA_FILE")" '{schema: $schema}')

curl -s -X POST \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  --data "$BODY" \
  "$REGISTRY_URL/compatibility/subjects/$SUBJECT/versions/latest" \
  | jq -e '.is_compatible == true' > /dev/null

echo "Schema is compatible"

Чек‑лист безопасной эволюции

  • Определите желаемый режим совместимости (обычно — назад совместимость для API и FULL_TRANSITIVE для событий).
  • Документируйте изменения в спецификации как код (OpenAPI/.proto/.avsc) и храните рядом с исходниками.
  • Используйте поля с умолчаниями и «обёртки» для явного отсутствия значения.
  • Резервируйте удалённые номера полей в Protobuf.
  • Запускайте автоматическую проверку совместимости в CI до сборки и деплоя.
  • Внедрите потребительские контракты для критичных интеграций.
  • Мерьте использование устаревших полей, планируйте и объявляйте «окно удаления» заранее.
  • В релизных заметках всегда указывайте изменения контрактов и статус устаревших полей.

Антипаттерны и грабли

  • Тихое изменение типов или форматов («вчера было число, сегодня строка»). Итог — скрытые сбои, неверные отчёты.
  • Перепрофилирование номеров полей в Protobuf. Итог — «призрачные» значения у старых клиентов.
  • Жёсткое удаление полей без периода совместимости. Итог — массовые 500/парсинг-ошибки у партнёров.
  • Отсутствие централизованной валидации. Итог — знание «как правильно» живёт только в головах разработчиков, ломается при первой ротации команды.
  • Версионирование «по папкам», но без регламента совместимости. Итог — v2 отличается от v1 «чем-то», догадайтесь сами.

Итоги

Стабильные контракты — это дисциплина инженерных решений и процессы вокруг них. Правильные режимы совместимости, спецификации как код, Schema Registry, контрактные тесты и обязательные проверки в CI позволяют выпускать изменения без паники и ночных откатов. Бизнес от этого получает предсказуемость сроков, меньше инцидентов и ускорение интеграций с клиентами и партнёрами.

Бонус: однажды вы выстроите этот конвейер — и каждый следующий контракт будет эволюционировать заметно дешевле и спокойнее, чем предыдущий.


контрактыprotobuf/avroверсионирование API