Documenting message queues and event streams as systematically as REST endpoints with OpenAPI
Once a system exchanges asynchronous messages over RabbitMQ, Kafka, or WebSockets, a description like OpenAPI's is missing: which messages flow over which channel, in what format, with what payload structure. AsyncAPI closes exactly that gap, with a specification deliberately modeled after OpenAPI.
Table of Contents
- 1. The documentation gap for asynchronous APIs
- 2. The basic structure of an AsyncAPI specification
- 3. Protocol agnosticism: one format for multiple transports
- 4. Code generation: deriving consumers and producers from the spec
- 5. AsyncAPI Studio and a visual channel overview
- 6. Versioning and breaking changes in event schemas
- 7. Combining AsyncAPI and OpenAPI in the same system landscape
- 8. A practical starting point for an existing Symfony project
- 9. AsyncAPI compared to other documentation approaches
- 10. Summary
- 11. FAQ
1. The documentation gap for asynchronous APIs
OpenAPI has become the binding standard for synchronous REST endpoints: every endpoint, every parameter, every response structure is described in a machine-readable way. Once a system instead or additionally communicates over message queues, event streams, or WebSocket channels, that description is usually missing entirely. New team members have to read consumer and producer source code to figure out which messages with what structure travel over a queue.
AsyncAPI carries the OpenAPI principle over into this asynchronous world: a specification that describes channels (queues, topics), the messages exchanged over them, and their payload schema, independent of the concrete transport protocol. The syntax is deliberately modeled after OpenAPI, so teams already using OpenAPI for REST find their way around quickly.
2. The basic structure of an AsyncAPI specification
An AsyncAPI file defines channels as the central concept, comparable to paths in OpenAPI. A channel represents a queue, a topic, or a WebSocket channel over which messages flow. For each channel, it is described whether it is used for sending (publish), receiving (subscribe), or both, from the perspective of the respective application. The separation between channel definition (where a message lands) and operation definition (what a specific application does with it) has been explicit since AsyncAPI 3.0, resolving ambiguities from earlier versions of the specification.
Every message references a payload schema, defined in JSON Schema format just like with OpenAPI, making it reusable and validatable. Headers, correlation IDs for message tracing, and example payloads can additionally be specified, so a new team member can assemble a working message from the specification alone. Reusable components under components/messages and components/schemas prevent the same payload structure from being defined redundantly, and potentially inconsistently, in multiple places across the specification.
# order-events.asyncapi.yaml
asyncapi: 3.0.0
info:
title: Order Events API
version: 1.0.0
description: Events around the order lifecycle in RabbitMQ
servers:
production:
host: rabbitmq.internal:5672
protocol: amqp
channels:
orderCreated:
address: orders.created
messages:
orderCreatedMessage:
$ref: '#/components/messages/OrderCreated'
operations:
publishOrderCreated:
action: send
channel:
$ref: '#/channels/orderCreated'
components:
messages:
OrderCreated:
payload:
type: object
required: [orderId, customerId, totalAmount]
properties:
orderId:
type: string
format: uuid
customerId:
type: string
totalAmount:
type: number
3. Protocol agnosticism: one format for multiple transports
An important conceptual difference from OpenAPI: AsyncAPI is deliberately designed to be protocol-independent. The same basic structure describes channels over AMQP (RabbitMQ), Kafka, MQTT, WebSockets, or even HTTP webhooks, with protocol-specific bindings as an additional, optional layer per server and channel.
That is especially valuable for systems that change transport protocol over time or use several protocols in parallel, for example RabbitMQ for internal services and WebSockets for live updates in the browser. The functional description of the messages stays stable, only the protocol-specific bindings change.
4. Code generation: deriving consumers and producers from the spec
As with OpenAPI, generator tools exist (the AsyncAPI Generator) that can produce boilerplate code for consumers and producers in various languages from a specification, including typed payload classes. For PHP projects the ecosystem here is noticeably thinner than for OpenAPI, which is why the specification in practice often serves primarily as documentation rather than a codegen source.
Even without automatic code generation, the documentation value stays high: a Symfony team can use the AsyncAPI specification as a binding reference to hand-build payload DTOs that match the described schema, and validate against that schema in contract tests. Such a contract test fails in a controlled way in the CI pipeline as soon as a producer accidentally drifts from the documented schema, instead of the error only surfacing later through a consumer silently failing in production.
<?php
// PHP: payload DTO matching the AsyncAPI specification
final readonly class OrderCreatedMessage
{
public function __construct(
public string $orderId,
public string $customerId,
public float $totalAmount,
) {
}
public static function fromArray(array $data): self
{
// Validation against the schema defined in the AsyncAPI spec
return new self(
orderId: $data['orderId'],
customerId: $data['customerId'],
totalAmount: (float) $data['totalAmount'],
);
}
}
5. AsyncAPI Studio and a visual channel overview
AsyncAPI Studio (the counterpart to Swagger UI for OpenAPI) renders a specification as interactive, searchable documentation in the browser: all channels, messages, and schemas laid out clearly, with expandable example payloads. For teams with many message types across multiple queues, this replaces the tedious route through scattered wiki pages or outdated diagrams. The Studio can also run locally as a Docker container or be published statically straight from a GitHub repository, so external integrators get the same view as the team itself.
A visual architecture diagram can also be generated automatically from an AsyncAPI specification, showing which services publish and consume which channels. That makes implicit dependencies between services visible that would otherwise only be recognizable by reading through several codebases.
6. Versioning and breaking changes in event schemas
Breaking changes in event schemas are trickier than in REST, because there are often multiple independent consumers processing a message, without the producer knowing exactly who is listening. A removed or renamed property in a message can silently break consumers, without the error becoming visible at publish time itself.
AsyncAPI specifications should therefore, just like OpenAPI specifications, be versioned and checked for compatibility against the previous version in a CI step. Additive, backward-compatible changes (new optional fields) are uncritical, removing or renaming existing fields requires a new message version with a parallel transition period.
7. Combining AsyncAPI and OpenAPI in the same system landscape
Most real systems are hybrid: synchronous REST endpoints for direct requests, asynchronous events for state changes that interest other services. There is no contradiction in maintaining both specification formats in parallel in the same repository, each for the part of the API they are built for.
A sensible pattern is to publish both specifications in the same documentation portal, so an external integrator finds both the REST endpoints and the relevant event channels in one place, instead of jumping between different, disconnected documentation sources.
8. A practical starting point for an existing Symfony project
The pragmatic starting point is not a complete specification for all existing queues, but a single, well-understood message type as a proof of concept. The team learns the syntax along the way and can assess whether the documentation effort is worth the benefit before describing the entire event landscape.
After that, it pays off to couple maintenance of the specification directly to the code, for example through a CI check that flags a missing update to the AsyncAPI file whenever the message handler code changes. Without that coupling, the specification tends to go stale just as fast as unversioned wiki documentation.
9. AsyncAPI compared to other documentation approaches
Besides AsyncAPI, there are other ways to document event schemas, with different trade-offs between effort, tooling maturity, and expressiveness.
| Approach | Tooling maturity | Protocol coverage | Typical use |
|---|---|---|---|
| AsyncAPI | Growing, good for docs | AMQP, Kafka, MQTT, WebSocket | Systematic event documentation |
| Schema Registry (e.g. Confluent) | Mature for Kafka | Primarily Kafka | Kafka-centric landscapes with Avro/Protobuf |
| Hand-written wiki | No tooling | Any | Small teams, few message types |
| Reading code only | No tooling | Any | Anti-pattern, but common status quo |
Mironsoft
OpenAPI design, Symfony APIs, and API security
APIs that external teams can integrate without back-and-forth questions?
We review existing REST APIs for inconsistent error formats, missing OpenAPI documentation, and security gaps, then build an API that is clearly documented, versioned, and hardened against abuse.
API Review
Checking the OpenAPI spec, error formats, and status codes for consistency.
Symfony Implementation
Using DTOs, Serializer, and Validator for clean, type-safe request/response models.
Security Audit
Hardening rate limiting, auth schemes, and input validation against real attack surfaces.
10. Summary
AsyncAPI: The Essentials at a Glance
Core idea
AsyncAPI carries the OpenAPI principle over to asynchronous communication: describing channels, messages, and payload schemas in a machine-readable way.
Protocol agnosticism
The same specification covers AMQP, Kafka, MQTT, and WebSockets, protocol-specific details are added as bindings.
Versioning
Event schemas need CI-checked compatibility rules, since breaking changes are especially tricky with multiple independent consumers.
Getting started
Start with a single, well-understood message type as a proof of concept, not documenting the entire event landscape at once.