Uma introdução a especificação AsyncAPI
Se você desenvolve ou consome APIs REST provavelmente já se deparou com alguma documentação escrita segundo a especificação OpenAPI. Ela é o padrão do mercado, apesar de eu preferir documentar usando o API Blueprint :)
Mas o assunto deste post é outra especificação, a AsyncAPI. Inspirada na OpenAPI, a AsyncAPI tem como propósito documentar aplicações que usam a arquitetura baseada em eventos (Event-Driven Architectures ou EDA). Na imagem a seguir podemos ver uma comparação entre os dois padrões:
Assim como sua irmã mais velha, a AsyncAPI permite que geremos documentação em diferentes formatos, bem como a geração de código, graças a uma série de ferramentas criadas pela comunidade.
Neste post vou demonstrar o uso da AsyncAPI com um exemplo bem simples. Vamos modelar um sistema baseado em microsserviços e EDA para uma empresa que vende um Software com Serviço (SaaS). O sistema é composto de três microsserviços:
-
accounts
: serviço responsável pelo cadastro de novos usuários. Ele gera o eventouser-registered
. -
subscription
: serviço responsável pelo controle da assinatura dos planos da empresa pelos usuários. Ele “ouve” o eventouser-registered
e pode gerar os eventosuser-subscribed
euser-unsubscribed
. -
finance
: serviço responsável pelo controle financeiro dos planos. Ele “ouve” os eventosuser-subscribed
euser-unsubscribed
e com base nestas informações ele pode gerar os eventospayment-succeded
epayment-failed
.
Neste contexto, quando eu uso o termo ouve
significa que o serviço assina (subscribe) o evento, ou ele é um consumidor (consumer) deste evento. E quando uso o termo gera
significa que o serviço publica (publish) um evento, ou ele é um produtor (producer) deste tipo de ocorrência. Estes conceitos são bem comuns em aplicações baseadas em eventos, como nosso exemplo. O Reactive Manifesto contém uma descrição bem interessante da diferença entre arquiteturas baseadas em mensagens e as baseadas em eventos:
Em uma arquitetura orientada a mensagens, o produtor conhece o consumidor. Em arquiteturas orientadas a eventos, por outro lado, o consumidor decide quais fontes deseja assinar.
Vamos agora começar a documentar nosso projeto. Para isso, criei um diretório chamado docs
e dentro dele o arquivo saas-service.yaml
. As primeiras linhas do arquivo contém:
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
Elas definem a versão da especificação que estamos usando, bem como algumas informações básicas.
Vamos agora incluir a informação relacionada a forma como vamos processar os eventos do projeto. A especificação é bem ampla em relação a isso, não limitando o usuário a uma solução em específico. Neste exemplo vamos considerar o uso do RabbitMQ, mas poderíamos usar Kafka, Mosquito, entre outros. Vamos incluir no nosso documento a configuração dos nossos servidores RabbitMQ:
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"
O próximo passo é incluirmos as informações relacionadas aos eventos que nosso sistema vai processar. Vamos começar com a publicação e assinatura do evento user-registered
:
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
Dentro de channels
vamos especificar os canais com as informações referentes ao que é necessário para os serviços que desejam publicar e consumir este evento. Como estamos usando o RabbitMQ como nosso message broker
as informações dentro de bindings
fazem referência a configurações específicas desta solução. Cada solução possui configurações especiais, como consta na documentação. Outro ponto importante é a chave message
que faz referência ($ref
) a uma mensagem que vamos definir agora:
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
O uso destas referências é útil para podermos reaproveitar a informação em vários eventos, caso necessário. Na documentação é possível ver os tipos de dados que a especificação suporta.
Após esta introdução, seguimos incluindo os outros eventos e o arquivo final ficou desta forma:
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
Gerando a documentação
Com o arquivo criado podemos agora criar uma documentação mais amigável. Para isso precisamos instalar o gerador, ou usar ele em sua versão Docker.
Para instalar localmente é necessário o npm
e executar o comando:
npm install -g @asyncapi/generator
Com o gerador instalado basta executar:
mkdir docs/html
ag docs/saas-service.yaml -o docs/html/ @asyncapi/html-template --force-write
Ou, usando o comando 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
A documentação gerada é bem útil e de fácil entendimento:
Também é possível gerar a documentação no formato Markdown.
Gerando código
O gerador também permite a geração de código. Segundo a documentação é possível gerar códigos em Node.js, Java e Python. Executei o comando abaixo:
ag docs/saas-service.yaml @asyncapi/python-paho-template -o src
E o diretório foi criado, com códigos na linguagem:
asyncapi_post ⟩ ls src/
README.md entity.py messaging.py
config-template.ini main.py payload.py
Como eu não sou fluente em nenhuma das três linguagens suportadas não vou tecer comentários sobre a qualidade do código gerado, deixo isso para o leitor testar e dar suas opiniões nos comentários do post.
Ferramentas
Além do gerador existe uma ferramenta online para testar a especificação, bem como Github Actions, parsers e plugins para VSCode e IDEs baseadas no IntelliJ.
Conclusões
É inegável que uma boa documentação é crucial para qualquer projeto, e ainda mais em casos de comunicação assíncrona e baseada em eventos. E ter um padrão de mercado, adotado por grandes empresas como Slack, SAP e Salesforce, aumenta muito a importância da AsyncAPI.
Minhas únicas reclamações são em relação ao fato de precisar escrever arquivos YAML (sou um velho ranzinza que não gosta do formato, mas isso é gosto pessoal) e a performance do gerador, que poderia ser mais rápido (talvez existam outras implementações em Go ou Rust, mas eu não fiz uma pesquisa extensa para este post).
De qualquer forma, me parece que aprender esta especificação é um bom investimento.