Модуль: Backend · Уровень: Middle+/Senior

TL;DR#

  • OpenAPI — машиночитаемая спецификация (контракт) HTTP/REST API в YAML/JSON. До версии 3.0 называлась Swagger (2.0 == “Swagger 2.0”). Swagger сейчас — это набор инструментов (Swagger UI, Swagger Editor, Codegen) от SmartBear, а сам формат — OpenAPI Specification (OAS) под управлением OpenAPI Initiative (Linux Foundation).
  • Версии: 2.0 (2014, Swagger) → 3.0 (2017, переработана структура: components, requestBody, servers) → 3.1 (2021, полная совместимость с JSON Schema Draft 2020-12).
  • Два подхода: contract-first (design-first) — сначала пишем спеку, из неё генерим типы/сервер/клиент (oapi-codegen); code-first — пишем Go-код с аннотациями, из него генерим спеку (swaggo/swag). Для контракта между командами/фронтом промышленный стандарт — contract-first.
  • Спека = source of truth. Контракт между бэкендом, фронтом, мобайлом, внешними потребителями. Версионируется в git, линтуется в CI (spectral, vacuum), на breaking changes проверяется (oasdiff).
  • Валидация: статическая (линт спеки в CI) + рантайм (middleware на базе kin-openapi валидирует request/response против схемы).

Теория#

Что такое OpenAPI#

OpenAPI Specification (OAS) — формальное, языконезависимое описание HTTP API. Описывает: эндпоинты, методы, параметры, тела запросов/ответов, схемы данных, аутентификацию, примеры. Главная ценность — единый машиночитаемый контракт, из которого можно:

  • генерировать серверные стабы и клиентов (десятки языков);
  • генерировать документацию (Swagger UI, Redoc, Stoplight);
  • валидировать трафик в рантайме;
  • проверять обратную совместимость;
  • использовать в mock-серверах (Prism) для параллельной разработки фронта и бэка.

История названий:

ЭраНазвание форматаКто ведёт
2010–2015Swagger SpecificationSmartBear
2016+OpenAPI Specification (OAS)OpenAPI Initiative (Linux Foundation)

«Swagger 2.0» и «OpenAPI 2.0» — синонимы. С 3.0 используют только «OpenAPI».

Версии: 2.0 vs 3.0 vs 3.1#

АспектOpenAPI 2.0 (Swagger)OpenAPI 3.0OpenAPI 3.1
Корневой ключswagger: "2.0"openapi: 3.0.xopenapi: 3.1.x
Серверыhost + basePath + schemesservers: [] (URL-шаблоны, переменные)то же
Тело запросаparameters с in: bodyотдельный requestBodyто же
Переиспользуемые объектыdefinitions, parameters, responsesединый componentsто же
Несколько content-typeодин на operationcontent: {application/json: ..., ...}то же
JSON Schemaподмножество Draft 4 (свои отклонения)расширенное подмножество Draft 5/«Wright»полный JSON Schema 2020-12
nullableнет (хаки через x-nullable)nullable: trueнет nullable; вместо него type: [string, "null"] (массив типов)
type как массивнетнетда (type: ["string","null"])
Webhooksнетнетда (webhooks)
examples (множ.)нетдада
Произвольный $schema/$idнетограниченнода

Ключевой сдвиг в 3.1 — это переход на JSON Schema 2020-12. Это значит, что схемы из OpenAPI 3.1 можно переиспользовать в обычных JSON Schema валидаторах и наоборот. Но это создаёт боль в тулинге: многие генераторы и валидаторы заточены под 3.0 и не понимают type: ["string","null"] или nullable-в-3.1-нет. На 2026 год часть production-инструментов всё ещё лучше работает с 3.0.x, поэтому версию выбирают исходя из зрелости тулчейна команды.

Структура спецификации#

Корневые объекты OpenAPI 3.x:

ПолеНазначение
openapiверсия спеки (строка, напр. 3.0.3)
infoметаданные: title, version (версия API, не спеки), description, contact, license
serversсписок базовых URL (с переменными окружения)
pathsобъект «путь → операции» (get, post, put, patch, delete, …)
componentsпереиспользуемые объекты: schemas, parameters, requestBodies, responses, securitySchemes, headers, examples
securityглобальные требования безопасности (ссылки на securitySchemes)
tagsгруппировка операций для документации

Внутри operation: operationId (уникальный, критичен для кодогенерации — из него генерится имя метода), summary, parameters (path/query/header/cookie), requestBody, responses, security, tags.

parameters — каждый имеет name, in (path|query|header|cookie), required, schema. Для in: path всегда required: true.

requestBodycontent с разбивкой по media-type, каждый со своей schema.

responses — ключи это HTTP-коды (200, 404, default), внутри description (обязателен!) + content + headers.

securitySchemes — типы: apiKey, http (basic/bearer), oauth2, openIdConnect, mutualTLS (3.1).

Пример YAML-спеки для CRUD-эндпоинта#

openapi: 3.0.3
info:
  title: Users API
  version: 1.2.0
  description: CRUD over users
servers:
  - url: https://api.example.com/v1
    description: production
  - url: http://localhost:8080/v1
    description: local

security:
  - bearerAuth: []

paths:
  /users:
    get:
      operationId: listUsers
      tags: [users]
      summary: List users
      parameters:
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        '200':
          description: page of users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createUser
      tags: [users]
      summary: Create user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: created
          headers:
            Location:
              schema: { type: string, format: uri }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          description: email already exists

  /users/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getUser
      tags: [users]
      responses:
        '200':
          description: user
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      operationId: updateUser
      tags: [users]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateUserRequest' }
      responses:
        '200':
          description: updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      operationId: deleteUser
      tags: [users]
      responses:
        '204':
          description: deleted
        '404': { $ref: '#/components/responses/NotFound' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  responses:
    BadRequest:
      description: validation error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: missing/invalid token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    User:
      type: object
      required: [id, email, createdAt]
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        name: { type: string, maxLength: 255 }
        createdAt: { type: string, format: date-time }
    CreateUserRequest:
      type: object
      required: [email]
      properties:
        email: { type: string, format: email }
        name: { type: string, maxLength: 255 }
    UpdateUserRequest:
      type: object
      properties:
        name: { type: string, maxLength: 255 }
    UserPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/User' }
        nextCursor: { type: string }
    Error:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
        details:
          type: array
          items: { type: string }

Contract-first vs code-first#

Contract-first (design-first): спека — артефакт, который пишут (часто фронт + бэк вместе) до кода. Из неё генерируют типы, server interface, клиентов. Спека ревьюится в PR как любой код.

Code-first: истина — Go-код с аннотациями (swaggo/swag) или со структурными тегами; спека генерируется из кода как побочный продукт.

КритерийContract-firstCode-first
Source of truthYAML-спекаGo-код / аннотации
Параллельная работа фронт/бэкотлично (mock из спеки сразу)плохо (нужен код)
Риск дрейфа кода и спекинизкий (код генерится из спеки)высокий (аннотации забывают обновить)
Гарантия валидности спекивысокаяспека может не валидироваться, генерится «как есть»
Барьер входавыше (надо знать OAS)ниже (пишешь привычный Go)
Контроль над сгенерированным кодомсреднийполный (код твой)
Кросс-командный контрактсильная сторонаслабая
Документацияпервичнавторична, легко отстаёт
Типичный инструмент в Gooapi-codegenswaggo/swag

Senior-выбор: для публичного/межкомандного API — contract-first. Для внутреннего сервиса, который никто кроме тебя не дёргает, и где спека нужна лишь для доки — допустим code-first, но осознавая риск дрейфа.

Кодогенерация в Go#

oapi-codegen (contract-first)#

oapi-codegen (форк/наследник deepmap/oapi-codegen, сейчас под oapi-codegen/oapi-codegen) генерирует из OpenAPI 3.x:

  • типы (Go-структуры из components/schemas);
  • server interface под нужный роутер: chi, echo, gin, net/http (std-http), fiber, gorilla;
  • HTTP-клиент (типизированные методы);
  • встроенную спеку (embedded-spec) для рантайм-валидации;
  • strict server — обёртка, где хендлеры принимают типизированный request-объект и возвращают типизированный response (роутер сам парсит/сериализует).

Конфиг (oapi-codegen.yaml):

# oapi-codegen.yaml
package: api
output: internal/api/api.gen.go
generate:
  models: true
  chi-server: true     # сгенерировать ServerInterface под chi
  strict-server: true  # строгий типизированный слой поверх
  embedded-spec: true
output-options:
  skip-prune: false
  user-templates: {}    # можно подменять шаблоны

Запуск через go generate:

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=oapi-codegen.yaml openapi.yaml

Сгенерированный ServerInterface (упрощённо, для chi):

// api.gen.go (фрагмент)
type ServerInterface interface {
    // (GET /users)
    ListUsers(w http.ResponseWriter, r *http.Request, params ListUsersParams)
    // (POST /users)
    CreateUser(w http.ResponseWriter, r *http.Request)
    // (GET /users/{id})
    GetUser(w http.ResponseWriter, r *http.Request, id openapi_types.UUID)
    // (PATCH /users/{id})
    UpdateUser(w http.ResponseWriter, r *http.Request, id openapi_types.UUID)
    // (DELETE /users/{id})
    DeleteUser(w http.ResponseWriter, r *http.Request, id openapi_types.UUID)
}

type ListUsersParams struct {
    Limit  *int    `form:"limit,omitempty"`
    Cursor *string `form:"cursor,omitempty"`
}

// Регистрирует роуты в chi.Router и вешает обработчики на ServerInterface.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { /* ... */ }

Со strict-server интерфейс становится чище — без http.ResponseWriter:

type StrictServerInterface interface {
    ListUsers(ctx context.Context, request ListUsersRequestObject) (ListUsersResponseObject, error)
    CreateUser(ctx context.Context, request CreateUserRequestObject) (CreateUserResponseObject, error)
    GetUser(ctx context.Context, request GetUserRequestObject) (GetUserResponseObject, error)
    // ...
}

// Имплементация:
func (s *Service) CreateUser(ctx context.Context, req CreateUserRequestObject) (CreateUserResponseObject, error) {
    u, err := s.users.Create(ctx, req.Body.Email, deref(req.Body.Name))
    if err != nil {
        if errors.Is(err, ErrEmailTaken) {
            return CreateUser409JSONResponse{Error{Code: "conflict", Message: "email exists"}}, nil
        }
        return nil, err // 500
    }
    return CreateUser201JSONResponse(toAPIUser(u)), nil
}

Тонкости oapi-codegen:

  • operationId обязателен и должен быть уникальным — иначе имена методов конфликтуют/мусорные.
  • Управление маппингом типов: x-go-type, x-go-type-import для подмены сгенерированного типа на свой (напр. кастомный Money, decimal.Decimal).
  • nullable vs required определяет, генерится поле как *T или T. Отсутствие required → указатель/omitempty.
  • additionalProperties: truemap[string]interface{} (или генерится спец-тип с AdditionalProperties).
  • oneOf/anyOf генерируются в union-тип с методами As.../Merge... — работать с ними неудобно, на senior это частый разговор о моделировании.

swaggo/swag (code-first)#

Генерирует Swagger 2.0 (не 3.x!) из аннотаций в комментариях Go. Это его принципиальное ограничение — для 3.x придётся конвертировать.

// @Summary      Create user
// @Tags         users
// @Accept       json
// @Produce      json
// @Param        body  body      CreateUserRequest  true  "payload"
// @Success      201   {object}  User
// @Failure      400   {object}  Error
// @Router       /users [post]
func (h *Handler) CreateUser(c *gin.Context) { /* ... */ }

swag init парсит AST и комментарии → docs/swagger.json + swagger.yaml + Go-пакет docs. Минусы: спека вторична, аннотации легко рассинхронизировать с реальным поведением, ограничены Swagger 2.0, нет строгой типобезопасности контракта.

Сравнение генераторов#

ИнструментПодходOAS-версияЧто генерит
oapi-codegencontract-first3.0/3.1типы, server iface, client, strict-server, embedded spec
swaggo/swagcode-first2.0спека из аннотаций + Swagger UI
ogencontract-first3.0/3.1типы + сервер + клиент, без рефлексии, встроенная валидация, OTEL
go-swaggerоба2.0сервер/клиент (тяжёлый, legacy на 2.0)

deepmap/oapi-codegen — старое имя; проект переехал в org oapi-codegen, импорт-путь github.com/oapi-codegen/oapi-codegen/v2. На собеседовании упоминание «deepmap» — маркер того, что человек давно с ним работает.

Валидация#

Два уровня:

1. Статическая (CI, линт спеки):

  • Spectral (Stoplight) — линтер OpenAPI/AsyncAPI/JSON Schema с настраиваемыми правилами (.spectral.yaml): обязательные operationId, описания, запрет additionalProperties без схемы, нейминг-конвенции и т.д.
  • vacuum — быстрый (Go) линтер, совместим с правилами Spectral, удобен для больших спек в CI.
# .spectral.yaml
extends: ["spectral:oas"]
rules:
  operation-operationId: error
  operation-description: warn
  oas3-unused-component: warn

2. Рантайм (request/response validation middleware): kin-openapi (getkin/kin-openapi) — Go-библиотека: парсит спеку (openapi3.Loader), строит роутер (gorillamux/legacy), валидирует входящие запросы и (опционально) исходящие ответы против схемы.

loader := openapi3.NewLoader()
doc, _ := loader.LoadFromFile("openapi.yaml")
_ = doc.Validate(loader.Context) // проверка самой спеки
router, _ := gorillamux.NewRouter(doc)

func ValidatorMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        route, pathParams, err := router.FindRoute(r)
        if err != nil { http.Error(w, "not found in spec", 404); return }
        reqInput := &openapi3filter.RequestValidationInput{
            Request: r, PathParams: pathParams, Route: route,
        }
        if err := openapi3filter.ValidateRequest(r.Context(), reqInput); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest); return
        }
        next.ServeHTTP(w, r)
    })
}

oapi-codegen интегрируется с kin-openapi: генерируемый embedded-spec + middleware oapimiddleware.OapiRequestValidator(swagger) даёт авто-валидацию запросов против встроенной спеки.

Response-валидация обычно включают только в тестах/staging (на проде дорого и рискованно ронять валидный для клиента ответ из-за расхождения схемы).

Source of truth и кросс-командное взаимодействие#

Спека — контракт. Практики senior-уровня:

  • Спека живёт в git (отдельный репо api-specs или рядом с сервисом), ревьюится в PR.
  • Из неё генерится: серверный код бэка, типизированные клиенты (фронт через openapi-typescript/orval, другие сервисы через oapi-codegen).
  • Mock-сервер (Prism, stoplight/prism) поднимается из спеки — фронт пилит UI, не дожидаясь бэка.
  • Документация (Redoc/Swagger UI) генерится автоматически — всегда актуальна.
  • Single source of truth убирает класс багов «фронт ждёт одно, бэк отдаёт другое».

Версионирование и breaking changes#

Версионирование API (не путать с версией спеки openapi:):

  • В пути: /v1/users, /v2/users — самый явный, грубый.
  • В заголовке: Accept: application/vnd.api+json; version=2 или кастомный X-API-Version.
  • Поле info.version (semver) — версия документа/API.

Breaking vs non-breaking:

Non-breaking (backward compatible)Breaking
добавление нового optional-поля в ответудаление поля из ответа
новый эндпоинтудаление/переименование эндпоинта
новый optional query-параметрновый required параметр
расширение enum в ответе (спорно для клиентов)сужение enum, смена типа поля
ослабление валидации входаужесточение валидации входа, новый required в body

oasdiff — инструмент для diff двух версий спеки и детекта breaking changes; ставят в CI как gate на PR:

oasdiff breaking old.yaml new.yaml --fail-on ERR
# выводит классифицированные изменения; ненулевой код → блокируем merge

Так контракт защищён: нельзя случайно сломать потребителей. Для намеренных breaking changes — бамп мажорной версии API и план миграции (deprecation через deprecated: true + Sunset/Deprecation заголовки).


Подводные камни / gotchas#

  • 3.1 ≠ просто «новее»: переход на JSON Schema 2020-12 ломает совместимость с тулингом, заточенным под 3.0. nullable в 3.1 нет — вместо него type: ["string","null"]. Многие генераторы/UI на 2026 ещё неполно поддерживают 3.1.
  • swaggo/swag отдаёт Swagger 2.0, а не OpenAPI 3.x. Если в требованиях «OpenAPI 3», то либо oapi-codegen/ogen (contract-first), либо конвертация 2.0→3.0 (теряются нюансы).
  • Дрейф code-first: аннотации забывают обновить — спека и код расходятся, документация врёт. В contract-first код генерится, дрейф невозможен (но можно забыть перегенерить — добавляют CI-проверку «generated up to date»).
  • operationId коллизии/отсутствие → у oapi-codegen мусорные/конфликтующие имена методов. Делать его обязательным правилом Spectral.
  • required ≠ nullable. required говорит «поле присутствует», nullable — «значение может быть null». В Go: required && !nullableT; иначе → *T/omitempty. Путаница ведёт к багам сериализации (отправка null vs отсутствие ключа).
  • additionalProperties: по умолчанию в JSON Schema разрешены любые доп. поля. Если хочешь strict — явно additionalProperties: false. Иначе валидация пропустит мусор; в Go генерится map, теряется типобезопасность.
  • oneOf/anyOf/allOf — мощно в схеме, но в Go генерируются в неудобные union-типы. allOf часто (неправильно) используют как «наследование/композицию»; для дискриминируемых union нужен discriminator.
  • Response-валидация в рантайме на проде может уронить корректный для клиента ответ из-за расхождения схемы. Держать в тестах/staging.
  • format (email, uuid, date-time) — это аннотация; не все валидаторы его проверяют, и не все генераторы мапят в нужный Go-тип. Проверять, что выбранный тулчейн действительно валидирует format.
  • $ref и циклические ссылки: глубокие/циклические $ref ломают часть генераторов; внешние $ref (на другие файлы/URL) поддерживаются неравномерно — часто бандлят в один файл (redocly bundle).
  • Версия спеки vs версия API: openapi: 3.0.3 — версия формата; info.version — версия твоего API. Их регулярно путают.
  • embedded-spec и расхождение: если валидируешь по встроенной (на момент сборки) спеке, а деплоят другую — поведение разойдётся; держать единый источник.

Вопросы на собеседовании#

В: В чём разница между Swagger и OpenAPI? О: Swagger — историческое название формата (версия 2.0 == «Swagger 2.0») и набор инструментов SmartBear (Swagger UI/Editor/Codegen). С версии 3.0 формат называется OpenAPI Specification и ведётся OpenAPI Initiative под Linux Foundation. То есть OpenAPI — это спецификация, Swagger сегодня — тулинг вокруг неё.

В: Что принципиально нового в OpenAPI 3.1 по сравнению с 3.0? О: Полная совместимость с JSON Schema Draft 2020-12. Исчез nullable (теперь type: ["string","null"]), type может быть массивом, появились webhooks, поддержка $schema/$id, mutualTLS. Минус — часть тулинга ещё не догнала 3.1, поэтому переход не бесплатный.

В: Contract-first vs code-first — что выберешь для API между несколькими командами и почему? О: Contract-first. Спека — единый source of truth, ревьюится отдельно, из неё генерится сервер и типизированные клиенты для всех потребителей, можно сразу поднять mock-сервер для параллельной разработки фронта. Code-first (swaggo) удобен для внутреннего сервиса, но спека вторична и склонна к дрейфу с кодом.

В: Что генерирует oapi-codegen и чем strict-server отличается от обычного? О: Генерирует Go-типы из components/schemas, ServerInterface под выбранный роутер (chi/echo/gin/std-http), HTTP-клиент, опционально embedded-spec. Обычный server interface даёт методы с http.ResponseWriter/*http.Request — парсинг/сериализация на тебе. Strict-server генерирует слой, где хендлер принимает типизированный RequestObject и возвращает типизированный ResponseObject (напр. CreateUser201JSONResponse), а инфраструктурный код сам парсит вход и сериализует выход — меньше boilerplate и ошибок.

В: Почему свежий проект на «OpenAPI 3» нельзя делать на swaggo/swag? О: swaggo/swag генерирует Swagger 2.0, а не OpenAPI 3.x. Для 3.x нужен contract-first генератор (oapi-codegen, ogen) либо конвертация 2.0→3.0 с потерей нюансов. Плюс swaggo — code-first, спека вторична и дрейфует.

В: Как валидировать запросы против OpenAPI-спеки в рантайме в Go? О: Через kin-openapi: загрузить и провалидировать спеку (openapi3.Loader, doc.Validate), построить роутер (gorillamux), в middleware найти маршрут (FindRoute) и вызвать openapi3filter.ValidateRequest. С oapi-codegen это упрощается через embedded-spec и OapiRequestValidator. Response-валидацию обычно держат в тестах/staging, не на проде.

В: Чем отличаются required и nullable и как это влияет на сгенерированный Go-код? О: required — поле обязано присутствовать в объекте; nullable — значение может быть null. Это ортогонально. В Go: required и не nullable → значение по значению (T); иначе → указатель (*T) с omitempty. Путаница приводит к багам: отправка null против отсутствия ключа, неверная (де)сериализация.

В: Как в CI не дать сломать обратную совместимость API? О: Линт спеки (spectral/vacuum) на качество и конвенции + diff против предыдущей версии (oasdiff breaking old new --fail-on ERR), который классифицирует изменения и блокирует merge при breaking. Breaking-изменения требуют бампа мажорной версии API и плана миграции (deprecated: true, Sunset/Deprecation заголовки).

В: Что считается breaking change в REST API? О: Удаление/переименование эндпоинта или поля ответа, добавление нового required-параметра/поля в запрос, ужесточение валидации входа, смена типа поля, сужение enum. Non-breaking: новый эндпоинт, новое optional-поле в ответе, новый optional-параметр, ослабление валидации.

В: Как подменить сгенерированный oapi-codegen тип на свой (например, decimal вместо float)? О: Расширениями x-go-type и x-go-type-import прямо в схеме, либо через секцию маппинга типов в конфиге. Это позволяет отдать в код, скажем, decimal.Decimal вместо float64 для денежных сумм, сохранив контракт в спеке.


На что копают на senior+#

  • Моделирование union/полиморфизма: oneOf/anyOf/allOf + discriminator, как это ложится на Go (генерируемые union-типы, As/Merge методы), когда лучше денормализовать схему ради удобства кода.
  • Эволюция контракта: стратегия версионирования (path vs header vs media-type), deprecation policy, Sunset header (RFC 8594), как катить breaking changes без даунтайма потребителей.
  • CI-гейтинг контракта: связка spectral/vacuum + oasdiff + проверка «generated code up to date» (перегенерить и git diff --exit-code), запрет merge при дрейфе/breaking.
  • Где валидировать: статика vs рантайм vs только-тесты; стоимость response-валидации на проде; что делать при расхождении embedded-spec и задеплоенной спеки.
  • Тулчейн-зрелость 3.0 vs 3.1: осознанный выбор версии под возможности генераторов/валидаторов команды, а не «берём новее».
  • Организация спек в масштабе: монорепо vs отдельный api-specs репо, бандлинг $ref (redocly bundle), переиспользование общих схем (error-модель, pagination) между сервисами.
  • Контракт как граница команд: mock-сервера (Prism) для параллельной разработки, генерация клиентов фронта (openapi-typescript/orval), consumer-driven contract testing (Pact) как дополнение к схеме.
  • Безопасность в спеке: корректное описание securitySchemes (oauth2 flows, scopes), что спека описывает контракт, но не заменяет реальную проверку прав; не утечь в публичную доку внутренние эндпоинты.
  • Производительность валидации: стоимость рантайм-валидации тяжёлых тел, кэширование скомпилированных схем, выбор ogen (без рефлексии) для горячих путей.