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

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

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

На некоторых порталах, в том числе и здесь, уже косвенно поднималась тема AsyncAPI и вызывала у авторов, как правило, смешанные чувства. Описывали преимущества и недостатки, приводили некоторые примеры. Справедливо отметить, что AsyncAPI, конечно же, не идеален.

Но если посмотреть на это с другой стороны: удобно ли будет разработчику читать ТЗ и другую документацию касаемо разработки интеграции по асинхрону, слепленную из кусков текста, таблиц, рисунков, непонятных ссылок и т.д.? Одно дело, если разработчик находится на проекте с самого начала и знает все тонкости и может сам что-то додумать. Но если человек придет на проект уже через один, два или более лет после начала проекта и увидит тонны макулатуры, которую как-то нужно читать и анализировать, то вот тут начинаются трудные времена.

Плюсом ко всему хотелось бы всем напомнить: когда много лет назад Open API только начинал внедряться, вспомните сколько было недовольств и возгласов: "Это нам зачем? Да она не всё поддерживает! Да для нее нормальных плагинов и библиотек нет! Да как это вообще можно применить на нашем технологическом стеке?"

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

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

В общем, стандартизация - наше всё!

Рисунок 1. Логотип AsyncAPI
Рисунок 1. Логотип AsyncAPI

Итак, поехали!

Начнем с определения. Что же такое AsyncAPI?

AsyncAPI – это спецификация для описания асинхронных API (Application Programming Interface), таких как те, которые используются в системах обмена сообщениями через брокеры сообщений (например, RabbitMQ, Kafka) или WebSocket. Эта спецификация позволяет стандартизировать описание интерфейсов взаимодействия между различными сервисами и приложениями, что упрощает разработку, интеграцию и документирование.

Со слов официального разработчика, AsyncAPI — это open source инициатива, которая стремится улучшить текущее состояние Event-Driven Architectures (EDA). Долгосрочная цель — сделать работу с EDA такой же простой, как и с REST API. Это касается документирования, генерации кода и т.д.

Как описывается?

Документацию AsyncAPI описывают в формате YAML или JSON. Но, как правило, YAML является предпочтительнее, так как он легче читается и поддерживает комментарии, что довольно-таки полезно для пояснений внутри документа (мы же все любим пояснения и детализацию, не так ли?)

Спецификация AsyncAPI определяет набор полей, которые используются в AsyncAPI-документе для описания API приложения. Хотя такой документ и может ссылаться на другие файлы для дополнительных подробностей или общих полей, обычно он служит единственным документом, который охватывает описание API в полном объеме.

Более того, AsyncAPI-документ служит контрактом для взаимодействия между получателями и отправителями в event-driven системе. Он определяет содержимое, необходимое для отправки сообщения службой и предоставляет для получателя ясное описание свойств сообщения.

Давайте перейдем к структуре самого документа и разберемся, насколько там все сложно (или легко) и как сильно отличается от нашего Swagger-a.

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

Части или блоки, из которых состоит документация AsyncAPI, принято называть корневыми элементами (root elements). Корневые элементы документа AsyncAPI предоставляют обзор характеристик и поведения API. Эти корневые элементы совместно определяют метаданные, каналы, компоненты и многое другое документа AsyncAPI. Они предоставляют полный обзор характеристик и поведения API.

 Рисунок 2. Структура документа AsyncAPI
Рисунок 2. Структура документа AsyncAPI

Поле info

Поле info предоставляет ключевые метаданные: название API, версию, описание, контактную информацию и лицензию. Это поле обеспечивает исчерпывающий обзор API, помогающий разработчикам, архитекторам и другим членам команды быстро усвоить его назначение и функциональность. Этот элемент является обязательным и часто служит для пользователя точкой входа в документацию API.

В поле info входят следующие поля:

  • title: название API

  • version: версия API

  • description: краткое описание назначения и функционала API

  • termsOfService: документ (или ссылка на него), определяющий условия предоставления

  • contact: контактная информация владельца API (имя, почта, URL)

  • license: информация о лицензии API (название и ссылка)

  • tags: тэги для категоризации и организации API-документации, а также для логической группировки приложений

  • externalDocs: ссылки на дополнительную документацию, связанную с этим API

Поле servers

Поле servers позволяет описать набор серверов, выделяя эндпоинты или брокеры сообщений, с которыми могут соединяться приложения. Это поле включает такую необходимую информацию как протокол, хост, порт и другие опции и обеспечивает связь между окружениями (production, staging, development и т.д.).

Вот некоторые поля, которые может содержать в себе одно поле servers:

  • host: имя хоста сервера, может включать порт

  • protocol: протокол, используемый сервером (например, AMQTP, MQTT, WebSocket)

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

  • pathname: путь к ресурсу хоста

  • description: описание сервера (необязательное поле)

  • title: понятное для человека название сервера

  • summary: краткое описание сервера

  • security: описание схем безопасности, которые используются на сервере

  • tags: перечень тэгов для группировки серверов

  • externalDocs: дополнительная документация для сервера

  • bindings: набор пар ключ-значение, в которых ключи содержат названия протоколов, а значения — специфические параметры протоколов, используемые сервером

Поле channels

С помощью поля channels можно задать список каналов, с которым взаимодействует приложение во время работы. Под каналами подразумеваются пути, по которым передаются сообщения. Можно описать назначение, адрес и ожидаемый формат сообщений.

Основные компоненты поля channels:

  • address: строковое представление адреса канала

  • message: схема сообщений, отправляемых по этому каналу

  • title: понятное для человека название канала

  • summary: краткое описание канала

  • description: развернутое описание, включающее контекст и подробности о сообщениях

  • servers: массив ссылок $ref на определения серверов, на которых доступен данный канал. Если это поле отсутствует или пустое, канал должен быть доступен на всех серверах, указанных в корневом поле servers

  • parameters: схема параметров, входящих в адрес канала

  • tags: перечень тэгов для группировки каналов

  • externalDocs: дополнительная документация для канала

  • bindings: набор пар ключ-значение, в которых ключи содержат названия протоколов, а значения — специфические параметры протоколов, используемые каналом

Поле operations

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

Основные компоненты поля operations:

  • action: send, если ожидается, что приложение будет отправлять сообщение в канал, и receive, если приложение должно ожидать приема сообщений в канале.

  • channel: ссылка $ref на определение канала, в котором выполняется операция

  • title: понятное для человека название операции

  • summary: краткое описание того, что делает операция

  • description: подробное описание операции

  • security: описание схем безопасности, которые связаны с этой операцией

  • tags: перечень тэгов для группировки операций

  • externalDocs: дополнительная документация для операции

  • bindings: набор пар ключ-значение, в которых ключи содержат названия протоколов, а значения — специфические параметры протоколов, используемые в операции

  • traits: список трейтов, применяемых к объекту операции

  • messages: массив ссылок $ref на объекты-сообщения (которые будут располагаться в корневом поле components), поддерживаемые этой операцией

  • reply: определение ответного сообщения

Поле components

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

Основные части поля components:

  • schemas

  • servers

  • channels

  • operations

  • messages

  • securitySchemes

  • serverVariables

  • parameters

  • correlationIds

  • replies

  • replyAddresses

  • externalDocs

  • tags

  • oppertaionTraits

  • messageTraits

  • serverBindings

  • channelBindings

  • operationBindings

  • messageBindings

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

Для начала придумаем кейс, по сценарию которого мы будем это всё реализовывать. Допустим, нам нужно обновить (возможно, частично) данные о пользователе в одном сервисе, потом передать эти данные через RabbitMQ в другой, а далее ещё и обновить в третьем, но уже стандартным HTTP-методом.

Итак, алгоритм работы предлагается следующим:

Для начала идёт получение запроса с фронта.

Далее на бэке в сервисе Service_1 (выступает в роли продюсера) отсылается сообщение в очередь "queue_1" формата:

{
  "payload":
    {
      "name":"Ivan",
      "address": "Moscow, Sofia str, 1B"
    },

  "user_id": "123456789"
}

Консумер в лице сервиса Service_2 забирает сообщение и делает следующее:

1) Обновляет данные пользователя в таблице users_table

2) Отправляет PATCH-запрос в Super_Service_3 /users/{user_id}

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

Переходим к описанию документации.

asyncapi: '3.0.0'

Это версия спецификации AsyncAPI, используемая в данной документации. В данном случае это версия 3.0.0.

info:
  title: User Update System
  version: '1.0.0'
  description: Асинхронный API для обновления информации о пользователе через RabbitMQ.

Этот блок содержит общую информацию о системе:

title — Название системы — "User Update System".

version — Версия API — 1.0.0.

description — Описание системы — «Асинхронный API для обновления информации о пользователе через RabbitMQ».

servers:
  production:
    host: amqp://rabbitmq-server:5672
    protocol: amqp
    description: Сервер RabbitMQ для обработки сообщений об обновлениях пользователей.

Блок servers определяет серверы, к которым можно подключиться для использования данного API. Здесь указан один сервер — production.

host — Адрес сервера — amqp://rabbitmq-server:5672, что указывает на использование протокола AMQP через порт 5672.

protocol — Протокол — amqp, то есть используется протокол AMQP.

description — Описание сервера — «Сервер RabbitMQ для обработки сообщений об обновлениях пользователей».

channels:
  queue_1:
    address: queue_1
    messages:
      userUpdated:
        $ref: '#/components/messages/UserUpdated'
    description: Очередь на прием данных на обновление пользователей.

Блок channels описывает каналы связи между системами. В данном случае у нас одна
очередь — queue_1.

address — Имя очереди — queue_1.

messages — Содержит описание сообщения, которое может быть отправлено или получено через эту очередь. Здесь указана ссылка на одно сообщение типа userUpdated, которое находится в разделе components/messages.

description — Описывает назначение канала — «Очередь на прием данных на обновление пользователей».

operations:
  publishUserUpdate:
    action: send
    channel: 
       $ref: '#/channels/queue_1'
    summary: Публикация обновления данных пользователя
    description: Сервис service_1 публикует сообщение об обновлении пользователя.

  consumeUserUpdate:
    action: receive
    channel: 
       $ref: '#/channels/queue_1'
    summary: Потребление обновления данных пользователя
    description: Сервис service_2 потребляет сообщение об обновлении пользователя.

Блок operations описывает операции, которые могут выполняться над каналами. В данном случае определены две операции.

publishUserUpdate:

action — Операция отправки (send) сообщения.

channel — Канал, на который отправляется сообщение — ссылка на канал queue_1.

summary — Краткое описание операции — «Публикация обновления данных пользователя».

description — Подробное описание операции — «Сервис service_1 публикует сообщение об обновлении пользователя».

consumeUserUpdate:

action — Операция приема (receive) сообщения.

channel — Канал, откуда принимается сообщение — ссылка на канал queue_1.

summary — Краткое описание операции — «Потребление обновления данных пользователя».

description: Подробное описание операции – «Сервис service_2 потребляет сообщение об обновлении пользователя».

components:
  messages:
    UserUpdated:
      name: UserUpdate
      title: Обновление пользователя
      summary: Сообщение с обновленными данными пользователя
      contentType: 'application/json'
      payload:
        type: 'object'
        properties:
          $ref: '#/components/schemas/UserUpdatePayload'

  schemas: 
    UserUpdatePayload:
      type: 'object'
      properties:
        payload:
          type: 'object'
          properties:
            name:
              type: 'string'
              description: 'Имя пользователя'
            address:
              type: 'string'
              description: 'Адрес пользователя'
        user_id:
          type: 'string'
          description: 'Id пользователя для поиска в таблице'

Блок components содержит компоненты, используемые в API. Он включает два раздела:

messages:

UserUpdated — Описание сообщения, которое передается при обновлении пользователя.

name — Имя сообщения — UserUpdate.

title — Заголовок сообщения — «Обновление пользователя».

summary — Краткое описание сообщения — «Сообщение с обновленными данными пользователя».

contentType — Тип содержимого — application/json.

payload — Ссылка на схему данных, которая описана в разделе schemas.

schemas:

UserUpdatePayload — Описывает структуру данных, передаваемых в сообщении.

Поле payload содержит объект с двумя свойствами:

name (тип string, описание: «Имя пользователя»).

address (тип string, описание: «Адрес пользователя»).

Также присутствует поле user_id (тип string), которое используется для идентификации пользователя в базе данных.

x-services:
  Service_1:
    description: Сервис управления данными пользователей
    operations:
      - $ref: '#/operations/publishUserUpdate'

  Service_2: 
    description: Сервис получения обновленных данных пользователей
    operations:
      - $ref: '#/operations/consumeUserUpdate'
    x-actions: 
      - update-users-table:
          description: Обновляет данные пользователя в таблице Users_table
      - send-super-service-3: 
          description: Отправляет PATCH-запрос в Super_Service_3 /users/{user_id} по полученному user_id

Блок x-services описывает сервисы, участвующие в обмене сообщениями. В данном случае описаны два сервиса:

Service_1:

description — Описание сервиса — «Сервис управления данными пользователей».

operations — Операции, выполняемые сервисом. Указано, что этот сервис выполняет операцию публикации обновления данных пользователя (ссылка на #/operations/publishUserUpdate).

Service_2:

description — Описание сервиса — «Сервис получения обновленных данных пользователей».

operations — Операции, выполняемые сервисом. Указано, что этот сервис выполняет операцию потребления обновления данных пользователя (ссылка на #/operations/consumeUserUpdate).

x-actions — Дополнительные действия, которые выполняются после получения сообщения:

update-users-table — Обновляет данные пользователя в таблице Users_table.

send-super-service-3 — Отправляет PATCH-запрос в Super_Service_3 по адресу /user/{user_id}, используя полученный идентификатор пользователя.

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

asyncapi: '3.0.0'
info:
  title: User Update System
  version: '1.0.0'
  description: Асинхронный API для обновления информации о пользователе через RabbitMQ.

servers:
  production:
    host: amqp://rabbitmq-server:5672
    protocol: amqp
    description: Сервер RabbitMQ для обработки сообщений об обновлениях пользователей.

channels:
  queue_1:
    address: queue_1
    messages:
      userUpdated:
        $ref: '#/components/messages/UserUpdated'
    description: Очередь на прием данных на обновление пользователей.

operations:
  publishUserUpdate:
    action: send
    channel: 
       $ref: '#/channels/queue_1'
    summary: Публикация обновления данных пользоателя
    description: Сервис service_1 публикует сообщение об обновлении пользователя


  consumeUserUpdate:
    action: receive
    channel: 
       $ref: '#/channels/queue_1'
    summary: Потребление обновления данных пользоателя
    description: Сервис service_2 потребляет сообщение об обновлении пользователя


components:
  messages:
    UserUpdated:
      name: UserUpdate
      title: Обновлене пользоателя
      summary: Сообщение с обновленными данными пользователя
      contentType: 'application/json'
      payload:
        type: 'object'
        properties:
        $ref: '#/components/schemas/UserUpdatePayload'


  schemas: 
    UserUpdatePayload:
        type: 'object'
        properties:
          payload:
            type: 'object'
            properties:
              name:
                type: 'string'
                description: 'Имя пользователя'
              address:
                type: 'string'
                description: 'Адрес пользователя'
          user_id:
            type: 'string'
            description: 'Id пользователя для поиска в таблице'

  x-services:
    Service_1:
      description: Сервис управления данными пользователей
      operations:
        - $ref: '#/operations/publishUserUpdate'


    Service_2: 
      description: Сервис получения обновленных данных пользователей
      operations:
        - $ref: '#/operations/consumeUserUpdate'
      x-actions: 
        - update-users-table:
            description: Обновляет данные пользователя в таблице Users_table
        - send-super-service-3: 
            description: Отправляет PATCH запрос в Super_Service_3 /users/{user_id} по полученному user_id

В AsyncAPI Studio (аналог SwaggerEditor-a) эта документация будет выглядеть следующим образом.

 Рисунок 3. Описанный документ в интерфейсе AsyncAPI Studio
Рисунок 3. Описанный документ в интерфейсе AsyncAPI Studio

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

Из преимуществ особо хотел бы выделить:

Стандартизация.

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

Поддержка различных протоколов.

AsyncAPI поддерживает широкий спектр протоколов обмена сообщениями, включая MQTT, AMQP, Kafka и другие. Это делает его универсальным решением для различных архитектур и сценариев использования.

Автоматическая генерация кода и документации.

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

Улучшение межкомандной коммуникации.

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

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

И я верю в его (и проектов аналогичного рода) перспективность, массовое внедрение и успех.

Ссылки на официальные источники:

За помощь в написании данной статьи выражаю огромную благодарность @avkekov

Комментарии (3)


  1. samizdam
    22.11.2024 20:12

    Не слыхал, спасибо.

    Действительно похоже на openapi. Стоит пробовать!


  1. anaxita
    22.11.2024 20:12

    Мы используем прото контракт сейчас для генерации ивентов в Кафку имаршвлим в json, но интересно почитать


  1. vic_1
    22.11.2024 20:12

    Ну это как я понимаю в сторонке от кода лежит, может и разъехаться. Да, и в Москве нет Софийской улицы, есть набережная с соответствующим названием