What's New in AsyncAPI v3.0

What’s New in AsyncAPI v3.0?

Posted in

The idea that all interfaces are APIs, whether RESTful, RPC, message-based, or something else, has been an accepted part of the API economy for many years. Nowhere is this more true than in the messaging world. In that context, AsyncAPI has become a significant feature of the landscape since its creation in 2016.

For the uninitiated, AsyncAPI is an API specification language for message-oriented communication. It has a very similar construct and syntax to OpenAPI, but it implements its own object definitions relevant to describing a message-based API. AsyncAPI has been of interest to us at Nordic APIs since the early days of the specification, featured both on our blog and at the Nordic APIs Summit.

In this post, we evaluate AsyncAPI v3.0, a new major version of the specification. This version promises to deliver valuable enhancements in how API providers describe their message-based APIs. There are several changes, so we are focusing on two “big ticket” items: Refactoring Channels and Operations and support for the Request-Reply pattern.

Refactoring Channels and Operations

The most notable change in AsyncAPI is the refactoring of Channels and Operations with the removal of the publish and subscribe properties from the Channel Object (previously called Channel Item). These properties are defined as an Operation Object. In version 2.x, they provided the means to differentiate between messages an API provider “subscribed to: (i.e., read from a queue) and those they “published” (i.e., wrote to a queue). The terms were used as a catchall without reflecting whether a publish-subscribe messaging approach was genuinely being used. They are declared inline and cannot be reused.

The publish and subscribe properties made sense from a reflection of some messaging topologies but not from the realities of how an API provider described their events and for whom. The terminology describes the actions of the API provider in the messaging infrastructure rather than what the API consumer can do. This arguably made the specification less usable as it appeared not to describe what operations are available to the API consumer. The lack of reuse for Channel Item objects and Operation objects also made API description documents written in OpenAPI more verbose than necessary.

For AsyncAPI to succeed, it must accurately reflect what is provided to the API consumer, hence the removal of these properties. Their replacement is a topology-agnostic mechanism that provides the means to describe and reuse operations independent of a channel. It is based on the decoupling of Channel and Operation objects.

Describing and Reusing Operations

The revised approach to specifying Channels and Operations provides five key changes:

  1. A Channel is defined separately from an Operation.
  2. A Channel defines all the messages that are sent or received at that Channel.
  3. A Channel is referenced by an Operation.
  4. An Operation specifies a subset of the messages a Channel defines.
  5. An Operation includes an action property, which can be either send or receive.

With these changes, the Channel has become the entity that captures information about features of the messaging layer, whereas the Operation captures what a given API consumer “can do” with the API. From the perspective of API consumers, this is an important shift as it helps emphasize use cases over features of the technical infrastructure.

Using the examples from the specification, the following is a Channel object, which specifies properties geared towards the technical implementation. For example, it includes parameters, servers, and protocol-specific bindings together with all the message definitions supported by the channel:

address: 'users.{userId}'
title: Users channel
description: This channel is used to exchange messages about user events.
messages:
  userSignedUp:
    $ref: '#/components/messages/userSignedUp'
  userCompletedOrder:
    $ref: '#/components/messages/userCompletedOrder'
parameters:
  userId:
    $ref: '#/components/parameters/userId'
servers:
  - $ref: '#/servers/rabbitmqInProd'
  - $ref: '#/servers/rabbitmqInStaging'
bindings:
  amqp:
    is: queue
    queue:
      exclusive: true
tags:
  - name: user
    description: User-related messages
externalDocs:
  description: 'Find more info here'
  url: 'https://example.com'

In comparison, the Operation object, defined with a suitable object name, identifies something that can be invoked on this channel, and only the specific channel messages that are used, identified using a Reference Object:

title: User sign up
summary: Action to sign a user up.
description: A longer description
channel:
  $ref: '#/channels/userSignup'
action: send
security:
  - petstore_auth:
    - write:pets
    - read:pets
tags:
  - name: user
  - name: signup
  - name: register
bindings:
  amqp:
    ack: false
traits:
  - $ref: "#/components/operationTraits/kafka"
messages:
  - $ref: '#/components/messages/userSignedUp'
reply:
  address:
    location: '$message.header#/replyTo'
  channel:
    $ref: '#/channels/userSignupReply'
  messages:
    - $ref: '#/components/messages/userSignedUpReply'

You’ll note there are properties in the Operation object example above that reflect the technical implementation. However, it is primarily focused on making operations easier for API providers to define and simpler for API consumers. This reuse of objects will also make API description documents written in AsyncAPI much less verbose.

Supporting the Request-Reply Pattern

The other notable enhancement to AsyncAPI v3.0 is formal support for the Request-Reply pattern, an extremely common messaging pattern. Many will be familiar with the Request-Reply pattern from Enterprise Integration Patterns by Hophe and Wolf. Being an architectural pattern, this obviously provides a generalized view of how a client might submit a request and expect a response to that request, using either synchronous or asynchronous behaviors to await a complete response or an acknowledgment that work is ongoing.

The Request part of this pattern is obviously represented by a Message Object as referenced by an Operation Object. AsyncAPI v3.0 introduces the Operation Reply Object to the Operation Object, allowing API providers to specify how the Reply queue can be resolved by an API consumer.

Again, taking an example from the v3.0 specification:

description: Consumer Inbox
location: $message.header#/replyTo

The API provider indicates to the API consumer that the reply queue value can be retrieved from the message header replyTo. The object implements a Runtime Expression, meaning the value can be set in the environment rather than being statically provided in an API description document. This obviously makes the document, and therefore the interface, much less brittle and allows deployments to different environments to be implemented without affecting the shape of the API.

The use of runtime expressions is common across OpenAPI and AsyncAPI, and it’s worth making the point that this approach is a boost for the flexibility of API description documents generally. Providing indicators that can be resolved at runtime — supported by appropriate tooling — means API providers can specify required behaviors rather than static values. This reduces brittleness and increases the chance that a given version of an API is supported for longer.

Final Thoughts: Trade-Offs In Using AsyncAPI v3.0

For the vast majority of users, the developments in AsyncAPI v3.0 will be welcome ones. The simplification of specifying operations without the baggage of using publish and subscribe properties makes the specification easier to comprehend. Decoupling Channels and Operations makes Channels reusable across Operations and simplifies the declaration of an Operation. Establishing the Request-Reply semantics also significantly benefits implementers of this pervasive asynchronous pattern.

These changes also have the potential for AsyncAPI to become widely used as a feature of enterprise integration. The ability to reuse objects more readily is enhanced by the fact that they can be sourced from both internal type definitions and externally from remote definitions. Allowing API producers to reuse definitions sourced from, for example, an enterprise-wide API repository provides critical information for purposes of API governance. Moreover, API providers can readily understand the impacts of change when they amend their API description documents written in AsyncAPI.

AsyncAPI, therefore, has the potential to be of significant benefit in the messaging landscape. With buy-in from tooling makers and vendors, it could become the spine of API governance in the enterprise messaging ecosystem.

As stated, we’ve focused on two key areas in our post. For a complete overview of updates in AsyncAPI 3.0.0, read the official release notes. For a comprehensive overview of migrating from AsyncAPI 2.x to AsyncAPI 3.0, read the official migration guide from AsyncAPI.