AsyncAPI – Documentation of event- and message-driven architectures

No Comments

Most applications today are distributed and loosely coupled – which in turn means that the architecture consists of many self-contained components that may be maintained by different teams. The information that is exchanged between components should be visibly documented and maintained. In the realm of REST APIs, there is already an existing standard: OpenAPI (see web page). AsyncAPI (see web page) is a new counterpart for event- and message-driven architectures which was developed on the basis of OpenAPI. From an OpenAPI perspective, this makes it easier to start using.
Compared to OpenAPI, AsyncAPI is more agnostic in its approach with regard to possible protocols. Currently, the following protocols are supported: AMPQ(S), HTTP(S), IBM MQ, JMS, (Secure) Kafka, (Secure) MQTT, STOMP(S), (Secure) WebSocket and Mercure. In order to make this article more than just an introduction, I would like to demonstrate the use of AsyncAPI in practice with a simple use case.

OpenAPI vs. AsyncAPI

Before we get into the use case in detail, I would like to take a closer look at two points: Firstly, I would like to draw a comparison between OpenAPI and AsyncAPI and secondly, I would like to shed light on the difference between events and messages. As already described in the introduction, AsyncAPI emerged from OpenAPI. One aim is to create as much compatibility as possible between the two specifications so that it is possible to reuse parts. Reusability makes a lot of sense, especially with regard to the data models within Components.

Comparison of OpenAPI and AsyncAPI — (c) https://www.asyncapi.com/docs/getting-started/coming-from-openapi

Figure 1. Comparison of OpenAPI and AsyncAPI —- © https://www.asyncapi.com/docs/getting-starten/coming-from-openapi

Figure 1 shows that the two specifications are really very similar in structure. In comparison to OpenAPI, channels are listed in the AsyncAPI. The channels are then also described protocol-agnostically in contrast to the paths. A complete description of the specification can be found at https://www.asyncapi.com/docs/specification/v2.1.0. After looking at the comparison between the two specifications, messages and events still need to be considered. The Reactive Manifesto contains a description of the difference, which I would like to briefly reproduce here. In a message-driven architecture, the producer knows the consumer. In event-driven architectures, on the other hand, the consumer decides which sources they want to subscribe to.

The use case

For the use case, a message regarding an order is to be transmitted from a producer via a queue to a consumer. In this case, RabbitMQ takes over the part of the message broker. The exact specification is now created via AsyncAPI. Any editor is sufficient to create the document. When using Spectral as a linter, it should be noted that it does not currently support the latest specification 2.1.0.

The specification

Every AsyncAPI-Spec starts with
asyncapi: '2.1.0'.

This is followed by the description of the info object.

info:
  title: Order Service
  version: 1.0.0
  description: The service is in charge of processing orders
  contact:
    name: Daniel Kocot
    email: daniel.kocot@codecentric.de
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
  rabbitmq:
    url: localhost:5672
    description: RabbitMQ
    protocol: amqp
    protocolVersion: '0.9.1'

As with OpenAPI, all information about the service is specified under info and servers. A key difference is the explicit specification of the protocol and its version in relation to the message broker under servers. The description of the service and the general setup is followed by the channel and the description of the respective operations. The specification contains a channel item object which refers to topics, routing keys, event types and paths.

channels:
  order-received:
    publish:
      operationId: orderReceivedPub
      description: Payload of received order
      message:
        $ref: '#/components/messages/order'
      bindings:
        amqp:
          timestamp: true
          ack: false
          bindingVersion: 0.2.0
    subscribe:
      operationId: orderReceivedSub
      description: Payload of received order
      message:
        $ref: '#/components/messages/order'
    bindings:
      amqp:
        is: routingKey
        exchange:
          name: orderExchange
          type: direct
          durable: true
          vhost: /
        bindingVersion: 0.2.0

The channel-item object just mentioned now describes all operations of the respective channel in detail. However, there are only two operation objects, publish and subscribe.

publishMessages that are consumed by the application via the channel.
subscribeMessages that are created by the application and sent to the channel.

Via bindings, protocol-specific configurations can be carried out on the server, channel, operation and message levels. The bindings each have their own versioning. This must also be explicitly specified. Now let’s take a concrete look at the configuration on the channel level. The is property defines the type of channel. This can be either queue or routingKey, which is also the default value. Depending on the value, either a queue or an exchange must be described. In our case, it means that the message is not forwarded directly to the consumer, but is first passed to the exchange, which directs the message to a specific queue. The binding briefly describes how the channel functions within the messaging component. Within the description of the channel we also find the payload. This references a Message object, which is also the last element of the specification.

components:
  messages:
    order:
      payload:
        type: object
        properties:
          id:
            type: integer
            format: int64
            description: ID of received order
          customerReference:
            type: string
            description: Reference for the customer according the order

Like the Schema object of the OpenAPI specification, the Message object is located below the Components object, but does not replace it in any way. Within the Components object, all reusable objects of a specification are grouped together and can be integrated via references. The AsyncAPI specification can also contain extensions, the Extensions. However, these are not used in the example.

Creating documentation

Now that we have worked our way through the specification for our use case, the question is how to turn this textual file into readable documentation for further use. Similar to OpenAPI, additional tooling is needed here. First, I would like to introduce the generator. In contrast to OpenAPI, the generator is written in JavaScript and is made available via npm or Docker. It can be used to create documentation in HTML or Markdown. It is also possible to generate initial snippets for certain programming languages and frameworks. We will disregard this option for the time being in this article. After the installation with npm npm install -g @asyncapi/generator we create a documentation artefact with the command ag.

api-showcases/async on  main [?] on ☁️
➜ ag order-service.yaml @asyncapi/html-template


Done! ✨
Check out your shiny new generated files at /Users/danielkocot/api-showcases/async.

If we now open the corresponding index.html, the documentation looks like in the screenshot below.

Generated AsyncAPI HTML Documentation

Besides using the generator, there are also various editors and IDE plug-ins to create previews of the specification on the fly. I currently use plug-ins for Visual Studio and IntelliJ.

In addition to HTML, we also have the option of generating a corresponding document in Markdown, for this we only need to use the Markdown template and we get the following result.

# Order Service 1.0.0 documentation

The service is in charge of processing orders
## Table of Contents

* [Servers](#servers)
* [Channels](#channels)

## Servers

### **rabbitmq** Server

| URL | Protocol | Description |
|---|---|---|
| localhost:5672 | amqp 0.9.1 | RabbitMQ |

## Channels

### **order-processed** Channel

#### `publish` Operation

Payload of processed order

##### Message

*Inform about a new processed order in the system*

###### Payload

| Name | Type | Description | Accepted values |
|---|---|---|---|
| id | integer | ID of received order | _Any_ |
| customerReference | string | Reference for the customer according the order | _Any_ |

> Examples of payload _(generated)_

```json
{
  "id": 0,
  "customerReference": "string"
}
```



#### `subscribe` Operation

Payload of processed order

##### Message

*Inform about a new processed order in the system*

###### Payload

| Name | Type | Description | Accepted values |
|---|---|---|---|
| id | integer | ID of received order | _Any_ |
| customerReference | string | Reference for the customer according the order | _Any_ |

> Examples of payload _(generated)_

```json
{
  "id": 0,
  "customerReference": "string"
}
```

Extending the documentation process with docToolchain

We can add the generated document to the existing architecture documentation in an extended docs-as-code toolchain with docToolchain. To do this, we run the generation command ag with other parameters.

❯ ag -o /Users/danielkocot/doc-showcases/src/docs order-service.yaml @asyncapi/markdown-template -p outFilename=order-service.md


Done! ✨
Check out your shiny new generated files at /Users/danielkocot/doc-showcases/src/docs.

Now we find the specification in our architecture documentation folder. Using docToolchain we can convert the Markdown file into the AsciiDoc format as a first step. I use version 2.0.0 for this, which has been released recently. Version 2.0.0 also introduces a CLI that facilitates the use of docToolchain. The call looks like this.

➜ ./dtcw exportMarkdown
dtcw - docToolchain wrapper V0.22
docToolchain V2.0.0
Java Version 11
docker available
home folder exists
use local homefolder install /Users/danielkocot/.doctoolchain/

> Configure project :
arc42/arc42.adoc

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

The converted file is available in the Build folder and can be incorporated from there into other AsciiDoc(tor) documents via include. This way we ensure that the specifications can be accessed in the process of creating an overall documentation.

Summary

We have seen that AsyncAPI opens up a possibility to make an event- or message-driven system understandable in a description language that can be read by humans and machines. In contrast to OpenAPI, however, the backend is known when writing the specification so that the peculiarities of the respective system can be addressed. Despite a version number greater than 1, the AsyncAPI spec is still under development. The tooling in the area of code generation is also currently still manageable, but is growing steadily. In connection with docToolchain, there are further possibilities with regard to integration into existing documentation. All in all, this is a very exciting topic that I will be coming back to time and again in the upcoming period.

Daniel joined the codecentric team in Solingen in October 2016. At the beginning working as a consultant with a focus on Application Lifecycle Management, his focus shifted more and more towards APIs. In addition to numerous customer projects and his involvement in the open source world around APIs, he is also a frequent speaker as an API expert.

More content about API Experience

Comment

Your email address will not be published. Required fields are marked *