Tooling Review: AsyncAPI

Documentation will set you free – this is a mantra often delivered by experienced developers, and it bears repeating whenever the value of documentation is discussed. Simply put, documentation is perhaps one of the most important elements of effective API developer communication, second only to active demonstrations, tutorials, and other interactive systems. Even then, in the best of implementations, those systems actively draw from the documentation generated by the developer, thereby providing an ever greater value.

We review AsyncAPI, a machine-readable specification.

It’s unfortunate, then, that not all documentation is created equal. While the value of documentation is well-known, the way in which documentation is created is as varied as the API industry itself. This is especially evident once you move out of the classical RESTful API microservices industry and into other types of API design trends. Today, we’re going to be reviewing a tool called AsyncAPI that has arisen from this very clear reality.

Created by previous Nordic APIs speaker Fran Méndez, AsyncAPI fills a gap between machine-readable documentation syntax for general APIs and the message-driven architectural design that still drives a wide range of services and systems. With this in mind, what exactly is AsyncAPI? What does it do, and more importantly how does it do it?

What AsyncAPI Does

AsyncAPI approaches message-driven documentation with two major core concepts assumed. First, it states that messages drive almost all API interactions, and that these messages are either an event or an action. While they encourage differentiation between those two types of messages, the separation is not one that is clearly defined or even required. Thus, everything that AsyncAPI does is rooted within the concept of a generally defined message system in which the message contains a header and a payload, both of which are optional, and can exist as either of the message types.

Second, AsyncAPI assumes that these messages are broadly sorted into a topic, typically using MQTT, AMQP, or STOMP, which are all “topic-oriented” syntax methods for defining what the messages concern. These are broadly analogous to URLs in classic HTTP APIs — when a message is sent to the API, it’s routed dependent on the topic as stated in your publishing order.

With these two assumptions, AsyncAPI begins to formulate a specification for translating this content to machine-readable formats in an efficient, effective way. AsyncAPI allows for the definition (or lack of definition) for any header in the message being sent, which allows for total protocol agnosticism. This allows for the machine-readable code that results from this process to be read across a wide variety of systems using MQTT, AMQP, or any of the other massively wide ranging methods to handle messages properly.

APIs can be described using either JSON or YAML for ultimate machine readability, and has the ability to tie into GUIs for human readability from the machine-generated and oriented code. This allows both machines and humans to read the code and understand what is happening, as well as identify both the type, contents, and topic of the message itself.

Finally, AsyncAPI is entirely open source. While arguments could be made for the value of both closed source and open source development, having an open source solution generally means greater security through communal inspection, greater efficiency through collective optimization, and more effective error checking through mass testing.

Read this post by Fran, AsyncAPI creator: Asynchronous APIs in Choreographed Microservices

Why Async API Helps

Machine-readable documentation has a ton of benefits. First and foremost, having machine-readable code means that the documentation which results will ultimately be easier to use as a source for other generated resources. This ultimately also means better code generation, as new structures and classes can be created automatically from the code. There is also the argument that working in a machine-readable system allows for easier and more effective scaling, as the most optimum amount of resources in the most optimum configuration can be found much easier in a more automated way.

All told, machine-readable documentation is hugely beneficial. When the developers of AsyncAPI wanted to leverage these benefits for their own internal systems, they found a gap between the classic HTTP APIs and RESTful design implementations of machine-readable implementations and the message-driven architecture of their internal systems. Because message-driven microservices are specific in terms of structure and approach, traditional solutions were not entirely a good fit for their implementations, and a new method was required.

This was the birth of AsyncAPI. The way the specification is formed allows for greater interoperability and tooling specifically for message-driven architectures, and as such, fills an often ignored sector of the API industry. For this reason alone, let alone its effective methodologies and efficient implementations, AsyncAPI is worth a look.

How Does AsyncAPI Work

Now that we understand where AsyncAPI helps, and specifically what gap it fills, how does AsyncAPI work? To begin with, it should be stated that the AsyncAPI is based generally on the JSON schema, and is composed of four parts: API Information, Servers, Topics, and Components. To begin with, a simple description file is created, typically called asyncapi.yml. This file then has the four syntax parts defined.

First, the API Information elements, also called the info object, needs to be defined. This object states the title of the API, its description, terms of service, contact details, and so forth. This is essentially what it says on the tin, and forms the basic identity of the API as a whole.

The info object looks something like this:

{
 "title": "AsyncAPI Sample App",
 "description": "This is a sample server.",
 "termsOfService": "https://asyncapi.org/terms/",
 "contact": {
   "name": "API Support",
   "url": "https://www.asyncapi.org/support",
   "email": "support@asyncapi.org"
 },
 "license": {
   "name": "Apache 2.0",
   "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
 },
 "version": "1.0.1"
}

Once the info object is stated, the Servers object needs to be created. This element defines the server urls, their descriptions, and the scheme that is used to handle the messages within the architecture. The statement of scheme is extremely important here, and allows for a protocol agnostic solution at the server level. This element typically looks like the example below, which uses MQTTS:

{
 "servers": [
   {
     "url": "development.gigantic-server.com",
     "description": "Development server",
     "scheme": "mqtts"
   },
   {
     "url": "staging.gigantic-server.com",
     "description": "Staging server",
     "scheme": "mqtts"
   },
   {
     "url": "api.gigantic-server.com",
     "description": "Production server",
     "scheme": "mqtts"
   }
 ]
}

Now that our servers are defined, as well as how the messages are handled on those servers, we can establish the Topics element. Topics are the essential method by which messages are bundled and organized, and as said before, are roughly analogous to HTTP API urls.

In the example below, a topic is defined for the AsyncAPI.

{
 "accounts.1.0.event.user.signup": {
   "subscribe": {
     "$ref": "#/components/messages/userSignedUp"
   }
 }
}

Finally, we can define Components. Components are sets of reusable objects that can be used to define and limit the messages that are pushed throughout the implementation. In the example below, we have several objects with varying types and formats that enable the addition of a user:

"components": {
 "schemas": {
   "Category": {
     "type": "object",
     "properties": {
       "id": {
         "type": "integer",
         "format": "int64"
       },
       "name": {
         "type": "string"
       }
     }
   },
   "Tag": {
     "type": "object",
     "properties": {
       "id": {
         "type": "integer",
         "format": "int64"
       },
       "name": {
         "type": "string"
       }
     }
   }
 },
 "messages": {
   "userSignUp": {
     "summary": "Action to sign a user up.",
     "description": "Multiline description of what this action does.\nHere you have another line.\n",
     "tags": [
       {
         "name": "user"
       },
       {
         "name": "signup"
       }
     ],
     "headers": {
       "type": "object",
       "properties": {
         "qos": {
           "$ref": "#/components/schemas/MQTTQoSHeader"
         },
         "retainFlag": {
           "$ref": "#/components/schemas/MQTTRetainHeader"
         }
       }
     },
     "payload": {
       "type": "object",
       "properties": {
         "user": {
           "$ref": "#/components/schemas/userCreate"
         },
         "signup": {
           "$ref": "#/components/schemas/signup"
         }
       }
     }
   }
 }
}

What is a Message-Driven API?

API code has had to ride the line between machine functionality and human understandability, largely because the methods that use the API are often human-interacted methods. In other words, humans use the APIs predominantly in an active way, and because of this, the code needs to be understood within that context. As an extension of that, APIs have long occupied a “request and receive” paradigm as discussed below, further extending the problem into modern implementations.

A message-driven API approach, then, is not really an “evolution” of these functions, but rather a sort of return to basics. AsyncAPI assumes that all API functions are necessarily “messages”, and as such, by approaching the API from a message-driven architecture, they can be better handled in a neutral manner rather than tooling the endpoints and internal mechanics towards the user.

Who Finds This Useful? What Does the Community Think?

With all of this in mind, we should identify exactly who benefits from this kind of approach. Broadly speaking, any API that utilizes message-driven architecture (and it seems that AsyncAPI would argue the majority of API functions can be considered “message-driven”) and desires machine-readable documentation could benefit from AsyncAPI.

Specifically, if an API’s functions can be distilled into the message archetype or expressed in such a format, the ability to generate further API code, documentation, specification notes, etc. is hugely valuable, and could result in some major gains.

Future Plans

Nordic APIs spoke with AsyncAPI creator Francisco Méndez Vilas to gauge what new features to expect in 2.0.0. Here’s what he had to say:

  1. A mechanism to explicitly denote from whose perspective the spec should be interpreted. As opposed to OpenAPI, the model in AsyncAPI is not client/server, so there is confusion trying to understand who can perform an operation. For instance, if your AsyncAPI document says “publish,” does it mean you can publish to this topic? Or, does it mean the API code will publish to this topic? The challenge here is that both answers are perfectly valid but we should stick to one.
  2. Improved support for IoT APIs. So far, we’ve been focusing on documenting asynchronous microservices. However, many people are using the spec to document their MQTT IoT APIs, and they have different needs, such as documenting binary data or byte sequences.
  3. Streaming APIs. We’ll add support for streaming APIs over HTTP, either using chunked transfer encoding or server-sent events.
  4. Events APIs. Some of the message-driven systems out there do not have the concept of “topic”, but instead all messages are sent through a single channel. WebSockets is a good example. We’ll provide a way to document events without the need for “topics”. A famous API using this kind of pattern is the Slack Real Time Messaging API.

Francisco also notes that there have been a few other contributors helping in the process of modeling version 2, such as Bruno Pedro, Mike Ralphson, Justin Karneges, among others.

Conclusion

AsyncAPI is an example of something that does one thing, and does it extremely well. It’s clear that AsyncAPI is meant specifically for machine readability and related functions.

Notably, AsyncAPI can certainly grant some huge gains and benefits to those searching for an option that allows for greater generation abilities. Because AsyncAPI is machine-centric and allows for code and documentation generation, it works well for automatic iteration, resulting in greater potential for growth and development.

Of course, AsyncAPI is not the end-all-be-all. While the argument could be made for “any API function is essentially a message”, that’s not always the case, especially with specialty APIs and distributed computational APIs. In such cases, AsyncAPI may be good for its machine readability, but not necessarily appropriate for a non-message driven system.

That being said, it is a great solution for what it does, and should be evaluated by those running message-driven systems and implementations, especially if they desire machine readability. What are your opinions on AsyncAPI? Let us know in the comments below!

More on Async API: