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:
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 theuser-registered
event.subscription
: service responsible for controlling the subscription of the company's plans by users. It "listens" for theuser-registered
event and can generate theuser-subscribed
anduser-unsubscribed
events.finance
: service responsible for the financial control of the plans. It "listens" for theuser-subscribed
anduser-unsubscribed
events, and based on this information, it can generate thepayment-succeded
andpayment-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:
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.