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/asyncapi
pnpm add -D @orpc/zodimport { AsyncAPIGenerator } from "@amqp-contract/asyncapi";
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { contract } from "./contract.js";
const generator = new AsyncAPIGenerator({
schemaConverters: [new ZodToJsonSchemaConverter()],
});
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",
},
},
});The converter is what turns your schemas into JSON Schema. Without a converter for a schema, generation fails — see allow unconvertible schemas to degrade to a placeholder instead.
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 ZodToJsonSchemaConverter()],
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 — converter support per library.