Generate AsyncAPI
@amqp-contract/asyncapi turns a contract into an AsyncAPI 3.1 document — the messaging counterpart to OpenAPI. Because it is generated from the contract your code already uses, it cannot drift from what the services actually do.
Generate a document
pnpm add @amqp-contract/asyncapiimport { AsyncAPIGenerator } from "@amqp-contract/asyncapi";
import { contract } from "./contract.js";
// Zod 4 and ArkType schemas convert themselves — no converter to configure.
const generator = new AsyncAPIGenerator();
export const spec = await generator.generate(contract, {
info: {
title: "Order Processing API",
version: "1.0.0",
description: "Type-safe AMQP messaging for order processing",
},
servers: {
production: {
host: "rabbitmq.example.com:5672",
protocol: "amqp",
description: "Production",
},
development: {
host: "localhost:5672",
protocol: "amqp",
description: "Local development",
},
},
});Each payload and headers schema becomes JSON Schema. A schema that implements Standard JSON Schema — it exposes ~standard.jsonSchema, as current Zod 4 and ArkType 2 releases do — converts itself, to draft-07 (the dialect AsyncAPI 3 schemas extend). Nothing needs configuring for those.
Convert Valibot and other schemas
A library without Standard JSON Schema — Valibot, today — needs a converter in schemaConverters. The oRPC converters fit as they are:
pnpm add -D @orpc/valibotimport { AsyncAPIGenerator } from "@amqp-contract/asyncapi";
import { experimental_ValibotToJsonSchemaConverter } from "@orpc/valibot";
const generator = new AsyncAPIGenerator({
schemaConverters: [new experimental_ValibotToJsonSchemaConverter()],
});Converters are a fallback: a schema that converts itself is never handed to them, and for the rest they are tried in order until one's condition matches. A converter is any object of the SchemaConverter shape exported by @amqp-contract/asyncapi — condition(schema, { strategy: "input" }) and convert(schema, { strategy: "input" }), returning [required, jsonSchema] — so you can write your own without depending on oRPC. A schema neither path can convert fails generation — see allow unconvertible schemas to degrade to a placeholder instead.
Set the virtual host
Every channel's AMQP binding records the RabbitMQ virtual host. It is / unless you say otherwise:
import { AsyncAPIGenerator } from "@amqp-contract/asyncapi";
const generator = new AsyncAPIGenerator({ vhost: "orders" });Write it to a file
// scripts/generate-asyncapi-json.ts
import { writeFileSync } from "node:fs";
import { spec } from "./generate-spec.js";
writeFileSync("asyncapi.json", JSON.stringify(spec, null, 2));For YAML:
// scripts/generate-asyncapi-yaml.ts
import { writeFileSync } from "node:fs";
import YAML from "yaml";
import { spec } from "./generate-spec.js";
writeFileSync("asyncapi.yaml", YAML.stringify(spec));{
"scripts": {
"generate:asyncapi:json": "tsx scripts/generate-asyncapi-json.ts",
"generate:asyncapi:yaml": "tsx scripts/generate-asyncapi-yaml.ts"
}
}Allow unconvertible schemas
An unconvertible schema fails generation by default (failOnMissingConverter defaults to true), so a missing converter is an error rather than a silent placeholder. To generate anyway — degrading unconvertible payloads to a generic { type: "object" } — opt out:
const generator = new AsyncAPIGenerator({
schemaConverters: [new experimental_ValibotToJsonSchemaConverter()],
failOnMissingConverter: false,
});Keep the default in CI, especially if anyone generates code from the document.
Improve the generated document
The generator can only publish what the contract carries, so descriptions come from defineMessage:
const orderMessage = defineMessage(orderSchema, {
summary: "Order created event",
description: "Emitted when a new order enters the system",
});Field-level descriptions come from the schema itself:
const orderSchema = z.object({
orderId: z.string().describe("Unique order identifier"),
amount: z.number().positive().describe("Total in the order's currency"),
});Both flow into the document, so documentation quality is a property of the contract rather than a separate artefact to maintain.
Keep it current in CI
Regenerate and diff, so a contract change that was not committed fails the build:
- run: pnpm generate:asyncapi:json
- run: git diff --exit-code asyncapi.jsonValidate it too:
npx @asyncapi/cli validate asyncapi.jsonUse the document
# Human-readable HTML docs
npx @asyncapi/cli generate fromTemplate asyncapi.json @asyncapi/html-template -o docs/
# Client code in another language
npx @asyncapi/cli generate fromTemplate asyncapi.json @asyncapi/python-paho-template -o clients/pythonAsyncAPI Studio renders a pasted document interactively, which is the fastest way to share a contract with someone who does not read TypeScript.
Codegen is also the practical answer to "how do non-TypeScript services join in?" — they get generated types from the same source of truth, though not the runtime validation.
Read cross-domain routing
Bridge exchanges appear on both channels, with a readable summary in the description and structure under x-amqp-exchange-bindings. See bridge domains.
Where next
- Define a contract — where
summaryanddescriptionlive. - Schema libraries — which libraries convert natively and which need a converter.