Documentation for your Event-Driven API
In this article, we will dive into the topic of event-driven APIs and how they can be documented using the tool called AsyncAPI. As companies increasingly rely on event-driven services for better performance, it is essential to understand how this mechanism works and how documentation using AsyncAPI can improve communication between services.
Event-driven services use a publisher-consumer model, which enables services to communicate through message brokers such as RabbitMQ or Redis. This model allows for asynchronous communication between services, meaning that a service can send a message to another service and continue with its own processing without waiting for a response. This is a critical feature in modern applications, where scalability and performance are crucial.
AsyncAPI offers a comprehensive and creative way of documenting event-driven APIs. It allows developers to describe the structure of messages exchanged between services, add metadata to messages, and define message channels. AsyncAPI provides a way for developers to understand the communication between services, which can be helpful in debugging applications and in onboarding new team members.
Real Case Scenario
We had to create a service that handles slug changes on specific entities, such as products or categories. The standard REST API communication cannot handle millions of requests and process data to update or create a new record in the database. This would overload the server with tons of requests, causing the system to crash at a certain point.
To solve this problem, we publish a new message (data) in RabbitMQ each time a product or category changes its slug. Related consumers will then receive these messages from queues and process them further.
We also needed a tool to document technical details of the internal structure, allowing other teams to understand the communication between services, such as queue names, routing keys, message bodies, and so on.
Implementation
As we have a lot of microservices, each service needs to document its own APIs, which should include available servers based on environment and channel details. Additionally, since versions change frequently, the documentation needs to be deployed continuously to Gitlab pages. Gitlab pipelines can be used to deploy changes automatically when a branch is merged to the master.
To render the YAML configuration, you can use the HTML generator of AsyncAPI. It will generate all documentation files, which should be located inside the docs/
directory. Then, create another folder inside named source/
, which will hold the YAML files of AsyncAPI.
Feel free to create another directory inside source/
to separate entity-level topics. For example, we created two additional directories for product/
and category/
.
Finally, create a new YAML file 0.0.1.yml
inside the directories created earlier to separate versions of the API.
The file structure should look like the following:
docs/
source/
product/
0.0.1.yml
category/
0.0.1.yml
Unfortunately, I am not allowed to expose the real docs that we used but the example from AsyncAPI Studio will be pretty enough to understand the use cases. Copy the example AsyncAPI configuration below and paste it inside the YAML files:
asyncapi: '2.4.0'
info:
title: Streetlights Kafka API
version: '1.0.0'
description: |
The Smartylighting Streetlights API allows you to remotely manage the city lights.
### Check out its awesome features:
* Turn a specific streetlight on/off 🌃
* Dim a specific streetlight 😎
* Receive real-time information about environmental lighting conditions 📈
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
servers:
test:
url: test.mykafkacluster.org:8092
protocol: kafka-secure
description: Test broker
security:
- saslScram: []
defaultContentType: application/json
channels:
smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured:
description: The topic on which measured values may be produced and consumed.
parameters:
streetlightId:
$ref: '#/components/parameters/streetlightId'
publish:
summary: Inform about environmental lighting conditions of a particular streetlight.
operationId: receiveLightMeasurement
traits:
- $ref: '#/components/operationTraits/kafka'
message:
$ref: '#/components/messages/lightMeasured'
smartylighting.streetlights.1.0.action.{streetlightId}.turn.on:
parameters:
streetlightId:
$ref: '#/components/parameters/streetlightId'
subscribe:
operationId: turnOn
traits:
- $ref: '#/components/operationTraits/kafka'
message:
$ref: '#/components/messages/turnOnOff'
smartylighting.streetlights.1.0.action.{streetlightId}.turn.off:
parameters:
streetlightId:
$ref: '#/components/parameters/streetlightId'
subscribe:
operationId: turnOff
traits:
- $ref: '#/components/operationTraits/kafka'
message:
$ref: '#/components/messages/turnOnOff'
smartylighting.streetlights.1.0.action.{streetlightId}.dim:
parameters:
streetlightId:
$ref: '#/components/parameters/streetlightId'
subscribe:
operationId: dimLight
traits:
- $ref: '#/components/operationTraits/kafka'
message:
$ref: '#/components/messages/dimLight'
components:
messages:
lightMeasured:
name: lightMeasured
title: Light measured
summary: Inform about environmental lighting conditions of a particular streetlight.
contentType: application/json
traits:
- $ref: '#/components/messageTraits/commonHeaders'
payload:
$ref: "#/components/schemas/lightMeasuredPayload"
turnOnOff:
name: turnOnOff
title: Turn on/off
summary: Command a particular streetlight to turn the lights on or off.
traits:
- $ref: '#/components/messageTraits/commonHeaders'
payload:
$ref: "#/components/schemas/turnOnOffPayload"
dimLight:
name: dimLight
title: Dim light
summary: Command a particular streetlight to dim the lights.
traits:
- $ref: '#/components/messageTraits/commonHeaders'
payload:
$ref: "#/components/schemas/dimLightPayload"
schemas:
lightMeasuredPayload:
type: object
properties:
lumens:
type: integer
minimum: 0
description: Light intensity measured in lumens.
sentAt:
$ref: "#/components/schemas/sentAt"
turnOnOffPayload:
type: object
properties:
command:
type: string
enum:
- on
- off
description: Whether to turn on or off the light.
sentAt:
$ref: "#/components/schemas/sentAt"
dimLightPayload:
type: object
properties:
percentage:
type: integer
description: Percentage to which the light should be dimmed to.
minimum: 0
maximum: 100
sentAt:
$ref: "#/components/schemas/sentAt"
sentAt:
type: string
format: date-time
description: Date and time when the message was sent.
securitySchemes:
saslScram:
type: scramSha256
description: Provide your username and password for SASL/SCRAM authentication
parameters:
streetlightId:
description: The ID of the streetlight.
schema:
type: string
messageTraits:
commonHeaders:
headers:
type: object
properties:
my-app-header:
type: integer
minimum: 0
maximum: 100
operationTraits:
kafka:
bindings:
kafka:
clientId: my-app-id
Then, install the HTML generator module for AsyncAPI:
npm install -g @asyncapi/generator
Once installation is completed, create a new script file at the root level of your project. It will automatically detect YAML files and render them using the AsyncAPI HTML generator to create all required files.
generate_doc.sh
for f in $(find ./docs/source/ -name '*.yml');
do file=$(echo $f | sed 's/.yml//;s/docs//;s/source//'); ag --force-write $f @asyncapi/html-template -o ./docs/generated/$file/;
done
Run the script ./generate_doc.sh
and it will create another directory named generated/
with all static files of your rendered documentation.
Deployment
Let's assume that there are three environments: dev
, pre-prod
, and prod
. At the very last stage, we need to deploy the documentation alongside the release.
To deploy the AsyncAPI docs via GitLab CI/CD, you can follow these steps:
- Create a folder named
.gitlab/ci
inside your project. - Add a file named
pages.yml
to this folder. This file will include CI/CD of deployment. - Insert the following code into the
pages.yml
file:
pages:
stage: docs
image: asyncapi/generator:1.9.5
script:
- cp -a ./docs/generated/. ./public/
artifacts:
paths:
- public
only:
refs:
- master
needs:
- pre-prod-deploy
This code will copy the generated docs (static files of generated docs, as mentioned in previous steps) to a folder named public
. The only
section specifies that the deployment should only happen when the master
branch is updated. Finally, the needs
section specifies that this deployment should happen after the pre-production deployment is completed.
Conclusion
Deploying the documentation is an essential step in the process of using AsyncAPI. By following the steps outlined above, you can easily deploy your documentation alongside your release. By continuously deploying your documentation, you ensure that it always reflects the latest version of your API, making it easier for other teams to understand the communication between services.
Overall, event-driven APIs and AsyncAPI offer exciting opportunities for developers to build modern applications that are scalable, performant, and well-documented.