An introduction to the AsyncAPI specification

If you develop or consume REST APIs, you have probably come across some documentation written according to the OpenAPI specification. It is the industry standard, although I prefer to document using API Blueprint :)

But the subject of this post is another specification, the AsyncAPI. Inspired by the OpenAPI, the AsyncAPI goal is to document applications that use the Event-Driven Architectures or EDA.

In the following image, we can see a comparison between the two patterns:

apidoc1

Like its big sister, the AsyncAPI allows us to generate documentation in different formats and generate code, thanks to a series of tools created by the community.

In this post, I will demonstrate the use of the AsyncAPI with a straightforward example. Let’s model a system based on microservices and EDA for a company that sells Software as a Service (SaaS). The system is composed of three microservices:

  • accounts: service responsible for registering new users. It generates the user-registered event.

  • subscription: service responsible for controlling the subscription of the company’s plans by users. It “listens” for the user-registered event and can generate the user-subscribed and user-unsubscribed events.

  • finance: service responsible for the financial control of the plans. It “listens” for the user-subscribed and user-unsubscribed events, and based on this information, it can generate the payment-succeded and payment-failed events.

In this context, when I use the term listen, it means that the service subscribes to the event, or it is a consumer of this event. And when I use the term generate, it means that the service publishes an event, or it is a producer of this type of occurrence.

These concepts are pretty standard in event-based applications, like our example. The Reactive Manifesto contains a concise description of the difference between message-based and event-based architectures:

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.

Let’s now start documenting our project. First, I created a docs directory and the file saas-service.yaml inside it. The first lines of the file contain:

asyncapi: 2.2.0
info:
  title: Awesome SaaS
  version: 1.0.0
  description: The Awesome Saas Company
  contact:
    name: Elton Minetto
    email: elton@minetto.dev

These lines define the version of specification we are using and some background information.

Let’s now include information related to how we will process project events. The specification does not limit the user to a specific solution. In this example, we will use RabbitMQ, but we could use Kafka, Mosquito, among others. So let’s include in our document the configuration of our RabbitMQ servers:

servers:
  rabbitmq-dev:
    url: localhost:5672
    description: Local RabbitMQ
    protocol: amqp
    protocolVersion: "0.9.1"
  rabbitmq-staging:
    url: staging-rabbitmq.server.saas.com:5672
    description: Staging RabbitMQ
    protocol: amqp
    protocolVersion: "0.9.1"
  rabbitmq-prod:
    url: rabbitmq.server.saas.com:5672
    description: Production RabbitMQ
    protocol: amqp
    protocolVersion: "0.9.1"

The next step is to include information related to the events that our system will process. Let’s start with publishing and subscribing to the user-registered event:

channels:
  user-registered:
    publish:
      operationId: userRegisteredPub
      description: The payload of user registration
      message:
        $ref: "#/components/messages/user"
      bindings:
        amqp:
          timestamp: true
          ack: false
          bindingVersion: 0.2.0
    subscribe:
      operationId: userRegisteredSub
      description: The payload of user registration
      message:
        $ref: "#/components/messages/user"
    bindings:
      amqp:
        is: routingKey
        exchange:
          name: userExchange
          type: direct
          durable: true
          vhost: /
        bindingVersion: 0.2.0

Inside channels, we will specify the way services can publish and consume this event. We are using RabbitMQ as our message broker, so the information inside bindings has specific configurations of this solution. Each solution has unique settings, as stated in documentation. Another important point is the message key that references ($ref) a message that we are going to define now:

components:
  messages:
    user:
      payload:
        type: object
        properties:
          id:
            type: integer
            format: int64
            description: ID of user
          name:
            type: string
            description: Name of user
          email:
            type: string
            description: E-mail of user
          password:
            type: string
            format: password
            description: Password of user
          registered_at:
            type: string
            format: date-time
            description: Timestamp of registration

The use of these references is helpful to reuse the information in various events, if necessary.

In the documentation, you can see all the data types that the specification supports.

After this introduction, we continued including the other events, and the final file looked like this:

asyncapi: 2.2.0
info:
  title: Awesome SaaS
  version: 1.0.0
  description: The Awesome Saas Company
  contact:
    name: Elton Minetto
    email: elton@minetto.dev
servers:
  rabbitmq-dev:
    url: localhost:5672
    description: Local RabbitMQ
    protocol: amqp
    protocolVersion: "0.9.1"
  rabbitmq-staging:
    url: staging-rabbitmq.server.saas.com:5672
    description: Staging RabbitMQ
    protocol: amqp
    protocolVersion: "0.9.1"
  rabbitmq-prod:
    url: rabbitmq.server.saas.com:5672
    description: Production RabbitMQ
    protocol: amqp
    protocolVersion: "0.9.1"
channels:
  user-registered:
    publish:
      operationId: userRegisteredPub
      description: The payload of user registration
      message:
        $ref: "#/components/messages/user"
      bindings:
        amqp:
          timestamp: true
          ack: false
          bindingVersion: 0.2.0
    subscribe:
      operationId: userRegisteredSub
      description: The payload of user registration
      message:
        $ref: "#/components/messages/user"
    bindings:
      amqp:
        is: routingKey
        exchange:
          name: userExchange
          type: direct
          durable: true
          vhost: /
        bindingVersion: 0.2.0
  user-subscribed:
    publish:
      operationId: userSubscribedPub
      description: The payload of user subscription
      message:
        $ref: "#/components/messages/subscription"
      bindings:
        amqp:
          timestamp: true
          ack: false
          bindingVersion: 0.2.0
    subscribe:
      operationId: userSubscribedSub
      description: The payload of user subscription
      message:
        $ref: "#/components/messages/subscription"
    bindings:
      amqp:
        is: routingKey
        exchange:
          name: subscriptionExchange
          type: direct
          durable: true
          vhost: /
        bindingVersion: 0.2.0
  user-unsubscribed:
    publish:
      operationId: userUnsubscribedPub
      description: The payload of user unsubscription
      message:
        $ref: "#/components/messages/unsubscription"
      bindings:
        amqp:
          timestamp: true
          ack: false
          bindingVersion: 0.2.0
    subscribe:
      operationId: userUnsubscribedSub
      description: The payload of user unsubscription
      message:
        $ref: "#/components/messages/unsubscription"
    bindings:
      amqp:
        is: routingKey
        exchange:
          name: unsubscriptionExchange
          type: direct
          durable: true
          vhost: /
        bindingVersion: 0.2.0
  payment-succeeded:
    publish:
      operationId: paymentSucceededPub
      description: The payload of successful payment
      message:
        $ref: "#/components/messages/payment"
      bindings:
        amqp:
          timestamp: true
          ack: false
          bindingVersion: 0.2.0
    subscribe:
      operationId: paymentSucceededSub
      description: The payload of successful payment
      message:
        $ref: "#/components/messages/payment"
    bindings:
      amqp:
        is: routingKey
        exchange:
          name: paymentSucceededExchange
          type: direct
          durable: true
          vhost: /
        bindingVersion: 0.2.0
  payment-failed:
    publish:
      operationId: paymentFailedPub
      description: The payload of failed payment
      message:
        $ref: "#/components/messages/payment"
      bindings:
        amqp:
          timestamp: true
          ack: false
          bindingVersion: 0.2.0
    subscribe:
      operationId: paymentFailedSub
      description: The payload of failed payment
      message:
        $ref: "#/components/messages/payment"
    bindings:
      amqp:
        is: routingKey
        exchange:
          name: paymentFailedExchange
          type: direct
          durable: true
          vhost: /
        bindingVersion: 0.2.0
components:
  messages:
    user:
      payload:
        type: object
        properties:
          id:
            type: integer
            format: int64
            description: ID of user
          name:
            type: string
            description: Name of user
          email:
            type: string
            description: E-mail of user
          password:
            type: string
            format: password
            description: Password of user
          registered_at:
            type: string
            format: date-time
            description: Timestamp of registration
    subscription:
      payload:
        type: object
        properties:
          id:
            type: integer
            format: int64
            description: ID of subscription
          user_id:
            type: integer
            format: int64
            description: ID of user
          plan_id:
            type: integer
            format: int64
            description: ID of plan
          plan_name:
            type: string
            description: Name of plan
          plan_value:
            type: number
            format: float
            description: Value of plan
          subscribed_at:
            type: string
            format: date-time
            description: Timestamp of subscription
    unsubscription:
      payload:
        type: object
        properties:
          id:
            type: integer
            format: int64
            description: ID of subscription
          unsubscribed_at:
            type: string
            format: date-time
            description: Timestamp of unsubscription
    payment:
      payload:
        type: object
        properties:
          id:
            type: integer
            format: int64
            description: ID of payment
          user_id:
            type: integer
            format: int64
            description: ID of user
          plan_id:
            type: integer
            format: int64
            description: ID of plan
          value:
            type: number
            format: float
            description: Value of payment
          created_at:
            type: string
            format: date-time
            description: Timestamp of payment

Generating the documentation

With the file created, we can now generate more friendly documentation. For this, we need to install generator or use it in its Docker version.

To install locally, you need npm and run the command:

npm install -g @asyncapi/generator

With the generator installed, just run:

mkdir docs/html
ag docs/saas-service.yaml -o docs/html/ @asyncapi/html-template --force-write

Or, using the command via Docker:

docker run --rm -it \
-v ${PWD}/docs/saas-service.yaml:/app/saas-service.yaml \
-v ${PWD}/docs/html:/app/output \
asyncapi/generator -o /app/output /app/saas-service.yaml @asyncapi/html-template --force-write

The generated documentation is handy and easy to understand:

apidoc1

It is also possible to create the documentation in Markdown format.

Generating code

The generator can also generate source code. According to documentation, it is possible to generate code in Node.js, Java, and Python. For example, I ran the command below:

 ag docs/saas-service.yaml @asyncapi/python-paho-template -o src

And the directory was created, with codes in the language:

asyncapi_post ⟩ ls  src/
README.md           entity.py           messaging.py
config-template.ini main.py             payload.py

As I am not fluent in any of the three supported languages, I will not comment on the quality of the generated code. Instead, I leave it to the reader to test and give their opinions in this post’s comments.

Tools

In addition to the generator, there is an online tool to test the specification and Github Actions, parsers, and plugins for VSCode and IDEs based on IntelliJ.

Conclusions

It is undeniable that good documentation is crucial for any project, even more so in asynchronous and event-based communication cases. And having a standard adopted by big companies like Slack, SAP, and Salesforce dramatically increases the importance of AsyncAPI.

My only complaints are:

  • the fact that I need to write YAML files (I’m a grumpy old man who doesn’t like the format, but that’s personal taste)
  • the generator could be faster (maybe there are other implementations in Go or Rust, but I didn’t do extensive research about this for this post).

Anyway, it seems to me that learning this spec is a good investment.