Skip to content

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 ​

bash
pnpm add @amqp-contract/asyncapi
typescript
import { 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:

bash
pnpm add -D @orpc/valibot
typescript
import { 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:

typescript
import { AsyncAPIGenerator } from "@amqp-contract/asyncapi";

const generator = new AsyncAPIGenerator({ vhost: "orders" });

Write it to a file ​

typescript
// 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:

typescript
// 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));
json
{
  "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:

typescript
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:

typescript
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:

typescript
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:

yaml
- run: pnpm generate:asyncapi:json
- run: git diff --exit-code asyncapi.json

Validate it too:

bash
npx @asyncapi/cli validate asyncapi.json

Use the document ​

bash
# 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/python

AsyncAPI 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 ​

Released under the MIT License.