Перейти к содержанию
V1 V2

Разработка OpenAPI-First с Kora

24 августа 2026

OpenAPI часто вводится как документация. Команда пишет контроллеры, DTO запросов, DTO ответов, аннотации валидации, аннотации безопасности и маппинги исключений, и только после того, как приложение уже существует, плагин проверяет этот код и выдаёт файл openapi.json. Swagger UI его отображает, клиентские генераторы могут его потреблять, и файл называется контрактом API.

Технически он описывает API. Архитектурно, однако, это не совсем контракт. Реализация — это контракт, а документ OpenAPI — лишь отчёт о том, что, по мнению фреймворка, реализация в настоящее время делает.

Kora поддерживает более сильную модель:

openapi.yaml
    ↓
Генератор Kora
    ↓
типизированные модели
типизированный серверный API
типизированный клиент
типизированные ошибки

В этой модели спецификация OpenAPI — не артефакт, реконструированный после реализации. Это авторитетный транспортный контракт, из которого можно генерировать обе стороны HTTP-границы. Сервер пишется против него. Клиент генерируется из него. Модели запросов и ответов выводятся из него. Коды статуса, ограничения валидации, схемы безопасности, медиатипы, multipart-поля и объявленные сбои — часть одного источника истины.

Это различие гораздо больше, чем выбор YAML вместо аннотаций. Оно меняет то, где происходит дизайн API, когда обнаруживаются проблемы совместимости, как координируются команды производителя и потребителя и сколько транспортной обвязки нужно поддерживать вручную.

Архитектура Kora особенно хорошо подходит для этой модели, потому что остальная часть фреймворка следует тому же паттерну: опишите намерение, проанализируйте структуру на этапе сборки, сгенерируйте читаемый типизированный исходник, проверьте его компилятором и держите механизмы времени выполнения малыми. Разработка OpenAPI-first просто расширяет эту философию через сетевую границу.


Важный вопрос — направление авторитета

Один и тот же документ OpenAPI может существовать в двух очень разных архитектурах.

В системе code-first:

реализация
    ↓
аннотации фреймворка и конвенции времени выполнения
    ↓
документ OpenAPI

Реализация владеет API. Спецификация описательна.

В системе contract-first:

документ OpenAPI
    ↓
сгенерированные контракты сервера и клиента
    ↓
реализация

Спецификация владеет API. Реализация ограничена ею.

Это центральное различие.

Контракт должен находиться над реализациями. Если два сервиса общаются через HTTP, ни одна сторона не должна реконструировать протокол независимо. Обе должны потреблять общее описание этого протокола, точно так же, как два класса внутри одного процесса могут компилироваться против одного и того же интерфейса Java.

Рассмотрим границу сервиса:

Сервис заказов
      ↓ HTTP
Сервис платежей

Слабая модель:

реализация контроллера заказов
      ↓
документация
      ↓
команда платежей вручную пишет совпадающий клиент

Более сильная модель:

        Контракт OpenAPI
        /              \
       ↓                ↓
сгенерированный сервер    сгенерированный клиент
       ↓                ↓
логика заказов       логика платежей

Протокол становится независимым архитектурным артефактом, а не случайным следствием модели контроллера одного фреймворка.


Почему OpenAPI должен быть контрактом, а не сгенерированным JSON

Генерация OpenAPI из аннотаций полезна. Она может производить документацию, Swagger UI, каталоги API и входы для генерации клиентов. Для многих небольших приложений этого может быть полностью достаточно.

Ограничение появляется, когда API — долгоживущая граница между независимо меняющимися системами.

Предположим, разработчик переименовывает поле DTO, потому что оно лучше читается в Java. Если OpenAPI генерируется из реализации, этот рефакторинг может молча стать внешним изменением протокола. Предположим, маппер исключений меняет код статуса. Контракт меняется, потому что изменилась реализация. Предположим, аннотация параметра контроллера меняет обязательность. Потребители могут видеть другой API, даже если никто явно не проверял изменение API.

Contract-first обращает это отношение.

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

Это восстанавливает фундаментальное свойство программной инженерии, которое удивительно легко потерять в микросервисах: инкапсуляцию.

Внутренние классы должны быть заменяемыми без изменения внешнего поведения.


Один контракт, две типизированные границы

Генератор OpenAPI Kora может создавать и декларативную серверную HTTP-инфраструктуру, и декларативные HTTP-клиенты из описания OpenAPI. Он также генерирует модели и инфраструктуру маппинга, необходимую для перемещения данных через эту границу.

Концептуально:

                    openapi.yaml
                    /          \
                   /            \
                  ↓              ↓
          сгенерированный сервер   сгенерированный клиент
                  ↓              ↓
          логика производителя     логика потребителя

Важное свойство не просто в том, что генерация кода экономит набор текста. Оно в том, что обе стороны выводят свою интерпретацию протокола из одного источника.

Без генерации факты протокола дублируются вручную:

HTTP-метод
путь
имена параметров пути
имена параметров запроса
заголовки
свойства JSON
обязательные поля
значения перечислений
типы содержимого
коды статуса
полезные нагрузки ошибок
требования безопасности

Каждый продублированный факт — возможность для расхождения.

Генерация меняет модель с дублирования на вывод.


Сервер реализует контракт вместо повторного его объявления

В контракт-первом сервере Kora код приложения не должен заново излагать HTTP-механику, которая уже присутствует в спецификации.

Если openapi.yaml говорит:

GET /users/{id}

с параметром пути int64 и ответами:

200 → User
404 → NotFoundProblem

то это уже факты протокола.

Сгенерированный серверный слой может перевести их в типизированный API Java или Kotlin. Код приложения может сосредоточиться на реализации поведения за операцией, а не на повторении маршрута, параметра, сериализации, валидации и метаданных ответа.

Упрощённая концептуальная форма может быть такой:

public interface UsersApiDelegate {
    GetUserResponse getUser(long id);
}
interface UsersApiDelegate {
    fun getUser(id: Long): GetUserResponse
}

Точный сгенерированный код зависит от опций генератора и спецификации, но архитектурное разделение — вот что важно:

OpenAPI:
определяет протокол

Сгенерированный слой Kora:
реализует транспортную механику

Написанный вручную делегат/сервис:
реализует бизнес-поведение

Это не даёт контроллерам стать второй поддерживаемой вручную спецификацией API.


Бизнес-код не должен описывать HTTP дважды

API code-first часто содержат несколько перекрывающихся описаний одной и той же конечной точки. Контроллер определяет путь и метод. Аннотации DTO влияют на схему. Аннотации валидации описывают ограничения полей. Аннотации безопасности описывают аутентификацию. Аннотации Swagger добавляют примеры и метаданные ответов. Обработчики исключений определяют ошибки.

Затем всё это преобразуется в OpenAPI.

Архитектура фактически поддерживает два языка для одной границы:

специфичные для фреймворка метаданные Java/Kotlin
        ↓
стандартные метаданные OpenAPI

Если OpenAPI уже является артефактом, который понимают клиенты, шлюзы, QA, системы документации, сканеры безопасности и внешние команды, сделать его исходным источником устраняет ненужную проблему синхронизации.

Транспортный контракт следует писать один раз.

Представление Java/Kotlin следует генерировать.


Сгенерированные модели устраняют расхождение DTO

Одна из самых распространённых проблем интеграции — несоответствие между моделями производителя и потребителя.

Сервер считает, что ответ таков:

{
    "userId": 123,
    "name": "Alice"
}

в то время как поддерживаемый вручную клиент предполагает:

{
    "id": "123",
    "displayName": "Alice"
}

Типизированные клиенты помогают, но они настолько же заслуживают доверия, насколько их связь с серверным контрактом.

С генерацией OpenAPI-first обе модели производителя и потребителя выводятся из одной схемы:

              схема
              /    \
             ↓      ↓
      модель сервера модель клиента

Им не нужно разделять один и тот же физический JAR или класс Java. На самом деле разделение JAR серверных DTO через границы сервисов может создавать нежелательную связанность реализации. Важно то, что проводные представления — проекции одного и того же нейтрального к языку контракта.

Это сохраняет автономию сервисов, уменьшая расхождение протокола.


Транспортные модели — не обязательно доменные модели

Сгенерированные модели OpenAPI описывают провод. Они не должны автоматически становиться всем бизнес-доменом.

Зрелый сервис может иметь:

CreateOrderRequest
OrderResponse
MoneyDto

на границе API, в то время как его домен использует:

Order
Money
CustomerId
OrderState

с более сильными инвариантами.

Здоровая архитектура поэтому может выглядеть так:

HTTP
 ↓
сгенерированный запрос OpenAPI
 ↓
маппинг
 ↓
доменная команда
 ↓
бизнес-логика
 ↓
доменный результат
 ↓
маппинг
 ↓
сгенерированный ответ OpenAPI

Для небольшого CRUD-сервиса это разделение может быть ненужным. Сгенерированные модели иногда можно использовать напрямую. Но для долгоживущих API явное разделение транспорт/домен предотвращает контроль внешних выборов протокола над внутренней архитектурой.

Подход Kora к маппингу на этапе компиляции делает такие границы относительно дешёвыми в поддержке.


Сгенерированный исходник должен быть заменяемым

Файл OpenAPI авторитетен только если сгенерированный код остаётся одноразовым.

Правило должно быть таким:

Изменение контракта и перегенерация никогда не должны разрушать написанную вручную бизнес-логику.

Сгенерированный вывод должен содержать транспортную механику:

модели
серверные интерфейсы/делегаты
адаптеры контроллеров
клиентские интерфейсы
маппинги ответов
склейку безопасности

Написанный вручную исходник должен содержать поведение приложения:

сервисы
доменную логику
репозитории
политики
реализации делегатов

Если разработчики вручную патчат сгенерированные классы, источник истины становится неоднозначным. YAML говорит одно, сгенерированный исходник говорит другое, а перегенерация становится опасной.

Сгенерированный исходник должен быть проверяемым, но не принадлежащим вручную.


Типы на этапе компиляции превращают изменения контракта в сбои сборки

Самое важное преимущество подхода Kora — не уменьшенный шаблонный код. Оно в том, что изменения контракта становятся видимыми для компилятора.

Предположим, схема ответа меняется с:

User

на:

UserDetails

После перегенерации сигнатуры серверного делегата и сгенерированного клиента меняются. Написанный вручную код, который всё ещё ожидает старый тип, может не скомпилироваться.

Этот сбой полезен.

Он означает:

ваша реализация больше не удовлетворяет объявленному внешнему контракту

Тот же принцип применяется к типам параметров, моделям запросов, вариантам ответов или обязательным полям.

HTTP обычно ослабляет статические гарантии, потому что производитель и потребитель разделены сетью. Генерация восстанавливает часть этой потерянной безопасности типов.


Ломающее изменение контракта должно быть болезненным рано

Представьте изменение:

GET /users/{id}

с целочисленного идентификатора на строку UUID.

В сырой HTTP-системе несколько кодовых баз могут продолжать компилироваться и завершаться ошибкой только когда трафик достигает изменённой конечной точки.

Со сгенерированным серверным и клиентским кодом перегенерация меняет типы методов. Сайты вызовов и реализации делегатов могут немедленно завершиться ошибкой.

Это ровно корректное поведение.

Транспортный эквивалент изменения интерфейса Java должен вести себя как изменение интерфейса Java: несовместимые реализации должны перестать компилироваться.

Contract-first разработка делает ломающие изменения API видимыми там, где их дешевле всего исправить.


Сгенерированные клиенты касаются авторитета, а не удобства

Сгенерированный HTTP-клиент часто представляется как инструмент продуктивности, потому что разработчикам не нужно писать URL, сериализовать JSON или переключаться по кодам статуса.

Это полезно, но более глубокое преимущество архитектурное.

Написанный вручную клиент — это вторая независимая интерпретация удалённого API.

Например:

POST /users
Content-Type: application/json

плюс:

if (status == 200) ...
else if (status == 404) ...
if (status == 200) ...
else if (status == 404) ...

дублирует знание протокола, уже принадлежащее производителю.

Сгенерированный клиент говорит нечто более сильное:

этот исходный код был выведен из того же контракта,
который определяет границу сервера

Разница не в меньшем количестве строк.

Она в меньшем количестве независимо поддерживаемых истин.


Типизированные ошибки — часть реального контракта

Многие API-стеки остаются строго типизированными только на счастливом пути.

Сгенерированный метод может предоставлять:

User getUser(...)
fun getUser(...): User

но каждый ответ, не являющийся 2xx, становится общим исключением, содержащим:

код статуса
сырое тело
заголовки

Вызывающий затем пишет обработку ошибок на основе целых чисел:

catch (HttpException e) {
    if (e.statusCode() == 404) {
        ...
    }
}
catch (e: HttpException) {
    if (e.statusCode == 404) {
        ...
    }
}

В этот момент API перестал быть типизированным ровно там, где потребителям больше всего нужна семантическая информация.

Ожидаемые ответы об ошибках — часть протокола.

API может намеренно объявлять:

200 User
404 NotFoundProblem
409 ConflictProblem
422 ValidationProblem

Это не эквивалентно:

сбой DNS
сброс соединения
сбой TLS
некорректный ответ

Сильный сгенерированный клиент должен сохранять это различие:

результат HTTP
├── объявленный успех
│   └── типизированная модель успеха
├── объявленный ответ ошибки/бизнеса
│   └── типизированная модель ошибки
└── необъявленный или транспортный сбой
    └── инфраструктурное исключение

Недавняя работа Kora по OpenAPI продолжала усиливать сгенерированные контракты ответов и типизированное поведение клиента/ошибок, потому что это фундаментально для того, чтобы сгенерированные клиенты были полезны за пределами тривиальных случаев успеха.


Коды статуса — часть системы типов

HTTP уже содержит дискриминатор ответа: код статуса.

Контракт должен использовать его намеренно.

Рассмотрим:

POST /orders

201 → Order
400 → ValidationProblem
409 → ExistingOrder

Потребитель должен иметь возможность рассуждать в терминах исходов протокола, а не сырых целых чисел и вручную декодированных тел.

Концептуально:

switch (result) {
    case Created created -> ...
    case BadRequest invalid -> ...
    case Conflict existing -> ...
}
when (result) {
    is Created -> ...
    is BadRequest -> ...
    is Conflict -> ...
}

Это делает объявленное HTTP-поведение частью API уровня исходника.

Общая модель «тело 2xx / всё остальное — исключение» теряет ценную информацию, которая уже присутствовала в OpenAPI.


Объявленные ошибки улучшают решения по отказоустойчивости

Типизированная семантика сбоев также важна для повторов и circuit breaker'ов.

409 Conflict может быть терминальным бизнес-результатом.

429 Too Many Requests может быть повторяемым после задержки.

Сетевой таймаут может быть повторяемым только если операция идемпотентна.

401 Unauthorized обычно требует исправления аутентификации, а не ещё одного идентичного запроса.

Если каждый сбой становится одним и тем же исключением, логика отказоустойчивости должна реконструировать смысл из кодов статуса и строк.

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

OpenAPI поэтому влияет на архитектуру надёжности, а не только на документацию.


Ожидаемый сбой и неожиданный сбой должны оставаться разными

Клиенту всё ещё нужен общий путь сбоя.

Если контракт объявляет:

200
404
409

но сервер возвращает:

502

это не объявленный бизнес-ответ.

Аналогично, некорректный JSON или транспортный таймаут не могут быть представлены объявленной моделью.

Хорошая сгенерированная граница сохраняет две категории:

исходы контракта

и:

нарушения протокола/транспорта

Схлопывание их сделало бы контракт менее заслуживающим доверия.

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


Безопасность принадлежит контракту

Безопасность — ещё одна область, где системы, ориентированные на реализацию, часто поддерживают несколько независимых источников истины.

Контроллер говорит, что аутентификация требуется. API-шлюз отдельно говорит, что аутентификация требуется. Документация говорит, что аутентификация требуется. Сгенерированный клиент имеет отдельную конфигурацию.

OpenAPI может объявлять схемы безопасности и требования безопасности на уровне операций напрямую.

Например:

components:
    securitySchemes:
        bearerAuth:
            type: http
            scheme: bearer

и:

security:
    -   bearerAuth: [ ]

Фреймворк Kora может использовать эти объявления при генерации точек интеграции сервера и клиента.

Это делает требования аутентификации видимыми до существования реализации и создаёт чёткую поверхность проверки для таких вопросов, как:

  • Публична ли эта операция?
  • Какой механизм аутентификации применяется?
  • Поддерживаются ли несколько механизмов?
  • Преднамерен ли анонимный запасной вариант?

Контракт не заменяет бизнес-логику авторизации, но он должен описывать соглашение о безопасности на транспортном уровне.


Безопасность контракта — не доменная авторизация

Документ OpenAPI может сказать:

требуется bearer-токен

Он не может содержательно выразить каждое правило вроде:

текущий пользователь может отменять только заказы, которыми владеет

Это принадлежит доменной/прикладной логике.

Чистое разделение:

аутентификация транспорта
→ OpenAPI + сгенерированная интеграция Kora

политика авторизации
→ приложение/домен

Это предотвращает превращение спецификации в замену бизнес-архитектуры.


Валидацию следует разделять так же

OpenAPI может выражать транспортную валидацию:

required
minLength
maxLength
pattern
minimum
maximum
enum
format

Kora может генерировать интеграцию серверной валидации из этих объявлений.

Это избегает определения одного и того же правила границы один раз в YAML и снова вручную в Java.

Но сложные бизнес-правила всё ещё принадлежат домену.

Например:

сумма должна быть положительной

может быть правилом транспорта/схемы.

В то время как:

сумма не должна превышать оставшийся кредитный лимит

требует бизнес-состояния и принадлежит глубже в приложении.

Конвейер становится:

валидация OpenAPI
→ структурно корректный запрос
→ бизнес-валидация
→ семантически корректная операция

Обязательное, опциональное и nullable — разные контракты

Один из самых лёгких способов создать плохие сгенерированные API — рассматривать все состояния отсутствия/null как эквивалентные.

Они разные:

обязательное и не-null
опциональное, но не-null при наличии
обязательное, но nullable
опциональное и nullable

Они могут иметь очень разные значения в Java, Kotlin, сериализации, семантике PATCH и обратной совместимости.

Для запроса обновления:

{}

может означать:

оставить displayName без изменений

в то время как:

{
    "displayName": null
}

может означать:

очистить displayName

Простое nullable-поле не обязательно может представлять оба состояния.

Разработка OpenAPI-first делает эту семантику намеренным вопросом дизайна API, а не случайностью сериализатора.

Генератор Kora предоставляет конфигурацию вокруг представлений nullable и опциональных, потому что проводное различие реально.


Схемы ошибок нуждаются в реальном дизайне

Команды часто тщательно проектируют модели успеха, рассматривая ошибки как запоздалую мысль:

500 → Error

Это даёт плохих клиентов.

Для каждой операции спросите:

  • Какие сбои ожидаемы?
  • Какие из них нормальные бизнес-исходы?
  • Какие повторяемы?
  • Какая схема возвращается?
  • Нужен ли потребителю стабильный машиночитаемый код ошибки?
  • Может ли он отличать недействительный ввод от конфликта?
  • Полезна ли информация корреляции?

Общая схема проблемы может быть ценной:

{
    "code": "ORDER_ALREADY_EXISTS",
    "message": "Order already exists",
    "traceId": "..."
}

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

Типизированные клиентские ошибки настолько же хороши, насколько контракт, который их определяет.


Машиночитаемые ошибки лучше разбора сообщений

Потребителям никогда не должна требоваться такая логика:

if message contains "already exists"

Человекочитаемые сообщения могут меняться.

Машиночитаемая семантика ошибок не должна.

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

Contract-first дизайн вынуждает эти решения в открытое.

Именно там они и должны быть.


OpenAPI-First обеспечивает реальную проверку совместимости

Как только файл OpenAPI становится первичным контрактом, CI может сравнивать:

предыдущий контракт
vs
новый контракт

и обнаруживать потенциально ломающие изменения до того, как реализация уйдёт в продакшн.

Примеры включают:

  • удалённые операции;
  • удалённые типы ответов;
  • несовместимые изменения схем;
  • новые обязательные поля запросов;
  • суженные перечисления;
  • изменённые местоположения параметров;
  • изменённые требования безопасности.

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

Это гораздо сильнее, чем проверка сгенерированного вывода Swagger после того, как изменение контроллера уже было слито.


Совместимость должна быть шлюзом сборки

Сильный конвейер API выглядит так:

правка openapi.yaml
    ↓
валидация спецификации
    ↓
линт организационных правил
    ↓
сравнение совместимости
    ↓
генерация кода Kora
    ↓
компиляция
    ↓
тест

Контракт фактически компилируется дважды.

Сначала инструментарий API проверяет, является ли документ внутренне корректным и совместимым с политикой.

Затем генерация Kora/OpenAPI превращает его в типы Java/Kotlin, и обычный компилятор проверяет код приложения против этих типов.

Это очень мощный цикл обратной связи.


Проверка API происходит до того, как реализация становится дорогой

Предположим, предлагаемая конечная точка:

POST /payments

имеет несколько проблем:

  • неправильный статус успеха;
  • нет ключа идемпотентности;
  • расплывчатое представление ошибки;
  • ненужные nullable-поля;
  • раскрытые внутренние идентификаторы базы данных;
  • непоследовательное именование;
  • отсутствующая аутентификация.

Если API обнаруживается только после начала реализации контроллера, создания DTO, тестов и нижестоящей интеграции, редизайн дорог.

С contract-first разработкой:

дизайн
→ проверка
→ генерация
→ реализация

Эти проблемы можно исправить, пока они ещё текстовые изменения в спецификации.

Самое дешёвое время для починки API — до того, как существуют потребители.


Дифы контрактов — лучшие поверхности проверки API

Pull request контракта может показать что-то настолько прямое, как:

 responses:
   '201':
     ...
+  '409':
+    description: Order already exists
+    content:
+      application/json:
+        schema:
+          $ref: '#/components/schemas/ExistingOrder'

Рецензент немедленно видит, что внешнее поведение изменилось.

С разработкой, ориентированной на реализацию, то же семантическое изменение может быть разбросано по классу исключения, совету контроллера, DTO, аннотациям и тестам.

Диф OpenAPI — не просто документация.

Это архитектурный диф.


Contract-First обеспечивает параллельную разработку команд

Это одно из самых больших организационных преимуществ.

Предположим, Команда A строит сервис заказов, а Команда B его потребляет.

Последовательность code-first часто выглядит так:

Команда A реализует
    ↓
Команда A производит пригодное описание API
    ↓
Команда B пишет клиент

Зависимость последовательна.

Contract-first меняет её:

Команды соглашаются на OpenAPI
        ↓
        ├── Команда A генерирует сервер и реализует
        └── Команда B генерирует клиент и интегрирует

Обе команды могут работать параллельно.

Моки можно строить из спецификации.

Код потребителя может компилироваться до того, как существует реальный сервер.

Экономия календарного времени может быть гораздо больше, чем экономия генерации кода.


OpenAPI — лучшая кросс-языковая граница, чем аннотации

Kora может реализовывать сервер на Java или Kotlin, но потребители могут быть:

Go
TypeScript
Python
Swift
Kotlin Android
другой Java-сервис

Модель аннотаций контроллера, специфичная для фреймворка, имеет мало ценности для этих потребителей.

OpenAPI намеренно нейтрален к языку.

Это даёт полезную границу:

Kora может иметь своё мнение о реализации, в то время как внешний контракт остаётся нейтральным к экосистеме.

Это отличное свойство для сервисной архитектуры.


Не делитесь JAR серверных DTO как контрактом

Java-организации иногда избегают генерации клиентов, публикуя общую DTO-зависимость.

Например:

orders-api-models.jar

Потребители импортируют её напрямую.

Это выглядит типизированным, но создаёт связанность с:

  • Java;
  • структурой пакетов сервера;
  • процессом релиза сервера;
  • выборами сериализации;
  • классами реализации.

Версионированный контракт OpenAPI — более чистый интерфейс.

JVM-потребители всё ещё могут генерировать из него строго типизированные классы, но эти классы — транспортные проекции, а не общая реализация производителя.

Различие важно для независимости сервисов.


Артефакты контракта можно версионировать независимо

В зрелой платформе контракт OpenAPI можно публиковать как версионированный артефакт:

orders-api:2.4.0

Реализация сервера компилируется против этого контракта.

Потребители закрепляют совместимую версию и генерируют клиентов.

Это разделяет:

версию контракта API

от:

версию сборки сервера

Сервис может развёртывать десятки внутренних сборок без изменения своего внешнего API.

Потребителям не нужно следить за каждым релизом реализации.

Версионирование контракта напрямую делает это явным.


Несколько контрактов могут быть здоровее одного гигантского спецификации

По мере роста сервиса один массивный файл OpenAPI может стать организационным монолитом.

Иногда отдельные контракты яснее:

public-api.yaml
admin-api.yaml
internal-data-api.yaml

если у них разные потребители, требования безопасности или циклы эволюции.

Kora может поддерживать несколько сгенерированных контрактов API в одном приложении.

Вопрос должен быть об архитектурном владении, а не просто о размере файла.

Теги полезны для группировки операций, но тег не обязательно является независимой границей контракта.


Сгенерированные клиенты должны настраиваться во время выполнения

Контракт определяет:

какие операции существуют

но конфигурация развёртывания определяет:

где живёт удалённый сервис

Это разные задачи.

Один и тот же сгенерированный клиент должен быть пригоден для использования в:

local
test
staging
production

через конфигурацию клиента времени выполнения.

Не генерируйте отдельный исходный код только потому, что URL конечных точек различаются.

Статический протокол принадлежит времени сборки.

Топология окружения принадлежит времени выполнения.

Это тот же статический/динамический разрыв, который появляется по всему Kora.


Учётные данные — тоже состояние времени выполнения

OpenAPI может объявлять:

Bearer
API key
Basic
OAuth2

но секреты никогда не следует компилировать в сгенерированный исходник.

Генерация создаёт типизированную интеграцию аутентификации.

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

Опять же:

компилируйте структуру
держите живое состояние динамическим

Конфигурация генератора — часть воспроизводимости

Сгенерированный API зависит от большего, чем документ YAML.

Он также зависит от:

версии генератора
версии генератора Kora
опций генератора

Эти входы могут влиять на именование, nullability, контракты ответов, интеграцию безопасности и форму сгенерированного исходника.

Поэтому воспроизводимая сборка API должна их закреплять.

Реальная детерминированная единица:

контракт OpenAPI
+
версия генератора
+
конфигурация генератора

Обновление генератора следует рассматривать больше как обновление компилятора, чем как случайный бамп плагина.


Сгенерированный исходник — артефакт сборки и инструмент отладки

Более широкая философия Kora подчёркивает читаемый сгенерированный исходник.

Это особенно ценно для OpenAPI, потому что генерация кода в противном случае может ощущаться как магия.

Когда поведение неясно, разработчики должны иметь возможность проверять:

сгенерированный серверный обработчик
сгенерированную модель
сгенерированный клиент
сгенерированный маппер ответов

и следить за тем, что происходит.

Сгенерированный исходник служит двум целям:

специализация времени выполнения
+
объяснение

Он должен выглядеть скучно, предсказуемо и механично.

Это функция.


Обслуживание одного и того же контракта предотвращает расхождение документации

Поддержка управления OpenAPI в Kora может обслуживать готовые файлы OpenAPI и раскрывать UI документации.

Архитектурно важный момент в том, что сервис может публиковать тот же контракт, который использовался для генерации.

Направление остаётся:

openapi.yaml
    ├── генерирует код сервера/клиента
    └── публикуется как документация

а не:

работающий код
    ↓
фреймворк пытается реконструировать OpenAPI

Swagger UI или другое средство просмотра становится представлением авторитетного файла.

Документация не может расходиться независимо, потому что она и есть источник.


Документация — лишь один потребитель контракта

Как только OpenAPI первичен, многие системы могут его использовать:

генератор сервера Kora
генератор клиента Kora
Swagger/Scalar UI
проверка совместимости
мок-сервер
каталог API
сканер безопасности
контрактные тесты
генерация кода фронтенда
генерация кода мобильных приложений
валидация шлюза

Вот почему сведение OpenAPI к «Swagger-документам» тратит большую часть его архитектурной ценности.

Контракт — это машиночитаемое определение интерфейса.

Документация — лишь одна проекция.


Тесты чёрного ящика могут использовать сгенерированный клиент

Особенно сильный паттерн:

OpenAPI
   ↓
сгенерированный сервер
   ↓
упакованный сервис

OpenAPI
   ↓
сгенерированный клиент
   ↓
тесты чёрного ящика

Тесты взаимодействуют с сервисом через реальный HTTP, а не прямые вызовы контроллеров, всё ещё получая типы на этапе компиляции.

Это даёт и то, и другое:

реалистичную для производства границу
+
дружелюбный для разработчика типизированный тестовый код

Сервер и клиент разделяют происхождение контракта, но остаются отдельными компонентами времени выполнения.


Всё же тестируйте сырой протокол там, где это важно

Есть одна оговорка.

Если и сгенерированный сервер, и сгенерированный клиент разделяют один и тот же дефект или предположение генератора, тест, использующий только сгенерированную пару, может упустить проблему интероперабельности.

Важные публичные API также должны включать независимые утверждения:

  • сырые HTTP-тесты;
  • валидацию схем;
  • внешний клиентский инструментарий;
  • потребителя на другом языке, где это уместно.

Генерация уменьшает случайное несоответствие.

Она не снимает необходимость проверять фактический провод.


Контракт может управлять негативным тестированием

Схемы OpenAPI содержат полезную тестовую информацию.

Если поле объявляет:

minLength: 3
maxLength: 40

тесты могут прорабатывать граничные случаи.

Если операция требует bearer-аутентификации, тесты могут проверять поведение без аутентификации и с аутентификацией.

Если конечная точка объявляет 404 и 409, тесты могут намеренно производить эти исходы.

Чем богаче контракт, тем больше он становится исполняемой тестовой спецификацией.


ID операций и имена схем — исходные API

Поскольку Kora генерирует исходный код, именование в OpenAPI имеет значение.

Хорошо:

getUserById
CreateOrderRequest
ValidationProblem

Плохо:

usersIdGet
Request1
ErrorObject

Технически корректный контракт всё ещё может генерировать неприятные API.

Дизайнеры контрактов должны проверять сгенерированные клиентские и серверные интерфейсы и спрашивать:

Хотел бы я использовать этот исходный API каждый день?

Дизайн OpenAPI — это дизайн библиотеки.


Не используйте слабые схемы, когда форма известна

Схема вроде:

type: object
additionalProperties: true

может быть уместна для действительно динамических данных.

Использование её просто для избежания моделирования разрушает большую часть выгоды генерации.

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

Если контракт знает, что значение — перечисление, UUID, дата или структурированный объект, выражайте эту информацию.

Чем сильнее контракт, тем сильнее сгенерированный исходник.


Но не раскрывайте внутренние детали реализации

Сильная типизация не означает раскрытия дизайна базы данных.

Тот факт, что PostgreSQL использует столбец BIGINT, не означает автоматически, что публичный API должен раскрывать внутреннюю семантику хранения.

OpenAPI должен описывать релевантное для потребителя значение протокола.

Контракт — это граница, а не дамп внутренних моделей.


Типы запросов и ответов часто заслуживают разделения

Распространённое сокращение — использование одной схемы:

User

для операций создания, обновления и ответа.

Это может давать неловкую семантику.

Запрос создания может не принимать:

id
createdAt

Обновление может быть частичным.

Ответ может содержать производную информацию.

Contract-first дизайн делает отдельные схемы естественными:

CreateUserRequest
UpdateUserRequest
UserResponse

Сгенерированная система типов затем автоматически предотвращает некоторые недействительные комбинации.


Пагинация, идемпотентность и ошибки — решения уровня контракта

Кросс-командные конвенции API следует выражать там, где потребители могут их видеть.

Пагинация должна намеренно выбирать:

offset/limit
cursor
page token

Идемпотентные операции записи могут раскрывать заголовок Idempotency-Key.

API с ограничением скорости должны объявлять поведение 429 и любые подсказки повтора.

Это не детали реализации контроллера.

Это часть того, как потребители безопасно используют сервис.

Contract-first проверка помогает платформенным командам их стандартизировать.


OpenAPI-First не означает OpenAPI для всего

Kora также поддерживает написанные вручную декларативные HTTP-серверы и клиенты.

Это важно, потому что не каждая HTTP-граница заслуживает генерации кода.

Динамический прокси, сервис метаданных без схемы или крошечная приватная конечная точка могут быть яснее с прямой HTTP-абстракцией Kora.

OpenAPI-first сильнее всего, когда:

  • API стабилен;
  • его потребляют несколько команд;
  • совместимость важна;
  • задействовано несколько языков;
  • генерация клиентов даёт реальный рычаг.

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


OpenAPI — не язык моделирования домена

Спецификация описывает, что пересекает HTTP-границу.

Она не должна пытаться кодировать всю бизнес-архитектуру.

Не переносите схему базы данных, правила транзакций, реализацию рабочих процессов, дизайн репозиториев или каждый бизнес-инвариант в YAML.

Чистое разделение:

OpenAPI:
внешняя транспортная семантика

Сгенерированный код Kora:
транспортная механика

Домен/приложение:
бизнес-семантика

Конфигурация времени выполнения:
факты, специфичные для развёртывания

Это предотвращает превращение contract-first разработки в бюрократию, управляемую спецификациями.


Генератор — фронтенд компилятора

Самая полезная ментальная модель — рассматривать OpenAPI как транспортный DSL.

Генератор Kora компилирует этот DSL в исходник Java/Kotlin.

Затем обычный компилятор Java/Kotlin и обработка Kora на этапе компиляции продолжают конвейер:

OpenAPI
  ↓
генератор OpenAPI Kora
  ↓
типизированный исходник Java/Kotlin
  ↓
связывание Kora на этапе компиляции
  ↓
байткод JVM
  ↓
время выполнения

Это объясняет, почему OpenAPI-first так естественно подходит Kora.

Kora не интерпретирует спецификацию динамически при каждом запросе.

Он обрабатывает известную структуру заранее и производит специализированный код.

Тот же принцип появляется во внедрении зависимостей, генерации репозиториев, маппингах и AOP.


Время выполнения становится намеренно скучным

После генерации путь запроса прямолинеен:

HTTP-запрос
    ↓
сгенерированный маршрут/обработчик
    ↓
сгенерированный маппинг запроса
    ↓
типизированная модель
    ↓
написанный вручную делегат
    ↓
сервис приложения
    ↓
типизированный ответ
    ↓
сгенерированный маппинг ответа
    ↓
HTTP-ответ

Клиентская сторона зеркалит его:

сервис приложения
    ↓
сгенерированный типизированный клиент
    ↓
сгенерированный маппинг запроса
    ↓
HTTP-клиент Kora
    ↓
сеть
    ↓
сгенерированный маппинг ответа/статуса
    ↓
типизированный результат или типизированная ошибка

Времени выполнения не нужно заново обнаруживать контракт.

Оно выполняет код, сгенерированный из него.

Скучное время выполнения — хорошее время выполнения.


Практичный рабочий процесс Kora

Сильный проект Kora с OpenAPI-first может использовать простой жизненный цикл.

Сначала отредактируйте:

openapi.yaml

Определите операции, схемы, объявленные сбои, правила валидации и безопасность.

Затем запустите валидацию контракта и проверки совместимости.

Затем сгенерируйте исходник сервера и/или клиента Kora.

Затем скомпилируйте.

Затем реализуйте сгенерированные интерфейсы делегатов и сервисы приложения.

Затем запустите тесты, включая тесты чёрного ящика через сетевую границу.

Наконец, опубликуйте ту же спецификацию через сервис или портал API.

Важный момент в том, что каждый нижестоящий артефакт остаётся привязанным к одному и тому же контракту.


Pull request должен начинаться с дифа контракта

Для изменения API порядок проверки обычно должен быть:

1. Что изменилось внешне?
2. Совместимо ли это?
3. Хорошо ли спроектирован протокол?
4. Корректно ли реализация его реализует?

Contract-first pull request делает это естественным.

Рецензенты сначала проверяют openapi.yaml.

Сгенерированный код — механический вывод.

Написанные вручную изменения приложения объясняют, как реализовано новое поведение протокола.

Это гораздо чище, чем реконструкция поведения API из изменений реализации.


Платформенная команда может превратить это в проторённую дорогу

В организационном масштабе лучший результат — не вики, говорящая командам быть contract-first.

Это инструментарий.

Конвенция платформенного Gradle может стандартизировать:

расположение каталога OpenAPI
версию генератора
версию генератора Kora
конвенции пакетов
линтинг
проверки совместимости
регистрацию сгенерированных исходников
значения безопасности по умолчанию
конвенции ошибок
публикацию контракта

Затем сервисной команде нужно только предоставить свой контракт и поведение приложения.

Это превращает архитектурную дисциплину в путь по умолчанию, а не в повторяющееся локальное решение.


Почему это важно для CTO и платформенных команд

Стоимость HTTP API — не время, необходимое для написания первого контроллера.

Долгосрочная стоимость исходит из:

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

Contract-first разработка атакует эти стоимости структурно.

Производитель и потребитель больше не поддерживают независимые интерпретации протокола.

Kora добавляет ещё один слой рычага, превращая контракт напрямую в нативный для фреймворка типизированный код, так что архитектура не просто документируется, а компилируется.

Это становится всё важнее по мере роста числа сервисов и команд.


Почему это важно для разработки с помощью нейро-инструментов

нейро-агенты кодирования также выигрывают от явных сгенерированных контрактов.

Без них агенту, интегрирующему два сервиса, может понадобиться проверять:

  • контроллеры;
  • DTO;
  • документацию;
  • обработчики исключений;
  • конфигурацию безопасности;
  • примеры.

С разработкой OpenAPI-first внешний интерфейс уже сконцентрирован в одном машиночитаемом артефакте, а сгенерированный клиент раскрывает его как типы.

Цикл становится:

контракт
→ сгенерированный исходник
→ обратная связь компилятора
→ реализация

вместо:

проза документации
→ угаданный HTTP-код
→ отладка времени выполнения

Это меньшее пространство поиска делает сгенерированные API полезными не только для людей, но и для автоматизированного инструментария разработки.


Самое важное правило

Весь подход можно резюмировать в одном правиле:

Никогда не поддерживайте вручную один и тот же факт протокола в нескольких местах, если его можно сгенерировать из одного авторитетного источника.

Не поддерживайте независимо:

маршрут сервера
+
маршрут клиента
+
маршрут документации

Не поддерживайте независимо:

серверный DTO
+
клиентский DTO
+
схему OpenAPI

Не поддерживайте независимо:

маппинг ошибок сервера
+
переключатель статуса клиента
+
таблицу ошибок документации

Выберите контракт как источник.

Сгенерируйте проекции.

Для HTTP-системы Kora OpenAPI — естественный язык контракта.


Заключение

OpenAPI становится гораздо ценнее, когда он перестаёт быть документацией, сгенерированной после реализации, и становится входом, из которого генерируются границы реализации.

В архитектуре code-first:

код контроллера
    ↓
метаданные фреймворка
    ↓
OpenAPI

реализация владеет протоколом. Спецификация описывает, что произошло.

В архитектуре Kora с OpenAPI-first:

openapi.yaml
    ↓
Генератор Kora
    ↓
типизированные модели
типизированный серверный API
типизированный клиент
типизированные ошибки
    ↓
реализация приложения

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

Это гораздо сильнее, чем производство Swagger JSON.

Это превращает HTTP в нечто более близкое к скомпилированному интерфейсу между независимо развёрнутыми системами.

Преимущества накапливаются со временем. Дизайн API происходит до того, как реализация затвердевает вокруг плохих выборов. Производители и потребители могут работать параллельно. Транспортные модели перестают расходиться. Написанные вручную клиенты исчезают. Ломающие изменения становятся видимыми в дифах контрактов и могут блокироваться в CI. Внутренний рефакторинг больше не протекает автоматически в публичные схемы. Ошибки становятся явными исходами протокола, а не общими исключениями. Java, Kotlin, TypeScript, Go, мобильные приложения, шлюзы, тесты, инструментарий безопасности и документация — всё может работать из одного нейтрального к языку источника.

Kora особенно хорошо подходит для этой архитектуры, потому что генератор OpenAPI следует той же философии, что и остальная часть фреймворка:

объявите намерение
→ проанализируйте на этапе сборки
→ сгенерируйте читаемый типизированный исходник
→ проверьте компилятором
→ держите время выполнения тонким

Генератор OpenAPI поэтому не просто функция удобства. Это транспортно-граничный эквивалент внедрения зависимостей Kora на этапе компиляции, сгенерированных репозиториев, маппингов и аспектов.

Принцип тот же:

Если структура уже известна до времени выполнения, опишите её один раз, сгенерируйте повторяющиеся механизмы и позвольте компилятору проверить результат.

Для HTTP-API эта структура — контракт.

Контракт не должен быть файлом JSON, обнаруженным после того, как приложение было написано.

Он должен быть тем, против чего приложение пишется.