npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typespec-asyncapi

v0.2.0

Published

TypeSpec emitter for AsyncAPI 3.0 specifications

Readme

typespec-asyncapi

A TypeSpec emitter for AsyncAPI 3.0 specifications.

Status: MVP working. The emitter maps @service/@server namespaces, @channel/@publish/@subscribe operations, and message models (including nested/array/enum/optional properties, @header, @correlationId, and @message name overrides) to a full AsyncAPI 3.0 document. Request/reply (@replyChannel) and Kafka bindings (the AsyncAPI.Kafka decorators: @Kafka.key, @Kafka.groupId, @Kafka.clientId, @Kafka.topicConfig, @Kafka.schemaRegistry) have since landed — see examples/kafka-orders for a worked, parser-validated example exercising all of it. Pre-1.0: the decorator vocabulary may still change based on community feedback.

Why

TypeSpec has first-class emitters for OpenAPI, JSON Schema, and Protobuf — but not AsyncAPI. The request has been open since 2023 (microsoft/typespec#2463) with no roadmap commitment. This project fills that gap: author your event-driven API in TypeSpec, emit a valid AsyncAPI 3.0 document, and feed it to the AsyncAPI toolchain (Studio, Generator, Modelina) for docs and code.

The full viability assessment behind this project — TypeSpec emitter API stability, the concept mapping table, prior art post-mortem, risks, and the phased plan — lives in docs/engagements/typespec-asyncapi-viability/.

Usage

Install

npm install typespec-asyncapi

Then in your TypeSpec project's files: import "typespec-asyncapi"; and add typespec-asyncapi to your tspconfig.yaml emitters.

To work on the emitter itself from source:

git clone https://github.com/milehimikey/typespec-asyncapi.git
cd typespec-asyncapi
npm install
npm run build   # required before compiling any TypeSpec against the library

A TypeSpec example

import "typespec-asyncapi";
using AsyncAPI;

@service(#{ title: "Order Events API" })
@server("main", "broker.example.com:9092", "kafka", "Primary Kafka broker for order events")
namespace OrderEvents {
  enum OrderStatus { Placed, Confirmed, Shipped, Delivered, Cancelled }

  model OrderPlaced {
    @header @correlationId orderId: string;
    customerId: string;
    placedAt: utcDateTime;
    notes?: string;
  }

  model OrderStatusUpdated {
    @header @correlationId orderId: string;
    status: OrderStatus;
    updatedAt: utcDateTime;
  }

  @channel("orders.lifecycle")
  @publish
  op publishOrderPlaced(payload: OrderPlaced): void;

  @channel("orders.lifecycle")
  @subscribe
  op consumeOrderStatusUpdated(payload: OrderStatusUpdated): void;
}

This is a trimmed version of examples/kafka-orders/main.tsp, which also covers nested models, arrays, a parametrized channel address, and a @message name override — see that directory's README for how to compile it.

The emitted document (abridged)

{
  "asyncapi": "3.0.0",
  "info": { "title": "Order Events API", "version": "0.0.0" },
  "servers": {
    "main": {
      "host": "broker.example.com:9092",
      "protocol": "kafka",
      "description": "..."
    }
  },
  "channels": {
    "orders.lifecycle": {
      "address": "orders.lifecycle",
      "messages": {
        "OrderPlaced": { "$ref": "#/components/messages/OrderPlaced" },
        "OrderStatusUpdated": {
          "$ref": "#/components/messages/OrderStatusUpdated"
        }
      }
    }
  },
  "operations": {
    "publishOrderPlaced": {
      "action": "send",
      "channel": { "$ref": "#/channels/orders.lifecycle" },
      "messages": [{ "$ref": "#/channels/orders.lifecycle/messages/OrderPlaced" }]
    },
    "consumeOrderStatusUpdated": {
      "action": "receive",
      "channel": { "$ref": "#/channels/orders.lifecycle" },
      "messages": [{ "$ref": "#/channels/orders.lifecycle/messages/OrderStatusUpdated" }]
    }
  },
  "components": {
    "schemas": {
      "OrderStatus": {
        "type": "string",
        "enum": ["Placed", "Confirmed", "..."]
      }
    },
    "messages": {
      "OrderPlaced": {
        "name": "OrderPlaced",
        "payload": { "type": "object", "properties": { "...": "..." } },
        "headers": {
          "type": "object",
          "properties": { "orderId": { "type": "string" } },
          "required": ["orderId"]
        },
        "correlationId": { "location": "$message.header#/orderId" }
      }
    }
  }
}

The full, real output for examples/kafka-orders is checked into test/kafka-orders.golden.json and asserted byte-for-byte in test/e2e.test.ts.

Decorators

| Decorator | Target | Meaning | | --------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | @channel(address) | Operation | Channel address, e.g. "orders.placed" or "orders.{orderId}". | | @publish | Operation | This operation sends to its channel (AsyncAPI action: "send"). | | @subscribe | Operation | This operation receives from its channel (AsyncAPI action: "receive"). | | @server(name, host, protocol, description?) | Namespace | Declares a broker server. | | @message(name?) | Model | Optional message-name override (default: the model name). | | @header | ModelProperty | Property is a message header, not payload. | | @correlationId | ModelProperty | Property is the message correlation id. | | @replyChannel(address) | Operation | Required whenever the op has a non-void Model return type: turns the return model into a reply message and adds an AsyncAPI reply object. | | @securityScheme(name, config) | Namespace | Registers a named security scheme (components.securitySchemes.<name>), validated per its config.type. | | @serverSecurity(server, schemes) | Namespace | Attaches a security ref array (to registered @securitySchemes) to the named @server. | | @security(scheme) | Operation | Attaches a security ref array (to a registered @securityScheme) to the operation. |

Kafka bindings

using AsyncAPI; also brings the nested AsyncAPI.Kafka namespace into scope as Kafka, so its decorators are written @Kafka.<name>:

| Decorator | Target | Meaning | | ------------------------------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | @Kafka.key | ModelProperty | The property's own schema becomes the message binding's key (the property stays in the payload too). | | @Kafka.groupId(value) | Operation | Operation binding groupId: {type:"string", enum:[value]}. | | @Kafka.clientId(value) | Operation | Operation binding clientId: {type:"string", enum:[value]}. | | @Kafka.topicConfig(partitions, replicas) | Operation | Channel binding {partitions, replicas} on the op's channel. Conflicting values across ops sharing a channel: first wins + a kafka-config-conflict warning. | | @Kafka.schemaRegistry(url, vendor?) | Namespace | Server binding on every declared @server whose protocol is "kafka" (never on non-Kafka servers). |

A binding object only appears when a Kafka decorator actually produced content for it — never an empty bindings: {}. Every emitted binding carries bindingVersion: "0.5.0".

@service(#{ title: "Order Events API" })
@server("main", "broker.example.com:9092", "kafka")
@Kafka.schemaRegistry("https://schema-registry.example.com", "confluent")
namespace OrderEvents {
  model OrderPlaced {
    @Kafka.key
    customerId: string;
    placedAt: utcDateTime;
  }

  @channel("orders.lifecycle")
  @publish
  @Kafka.topicConfig(6, 3)
  op publishOrderPlaced(payload: OrderPlaced): void;
}

See examples/kafka-orders/main.tsp for the full worked example (which also adds @Kafka.groupId to a @subscribe operation) and test/kafka-orders.golden.json for its emitted bindings.

Security

@securityScheme(name, config) registers a named AsyncAPI 3.0 Security Scheme Object (emitted to components.securitySchemes); @serverSecurity(server, schemes) and @security(scheme) attach security reference arrays to a @server or an operation, respectively. Both servers.<n>.security and operations.<id>.security are plain arrays of {"$ref": "#/components/securitySchemes/<name>"} — never the 2.x named-requirement-map shape, which fails the AsyncAPI 3.0 JSON Schema despite appearing in the spec's own example.

Each of the 13 concrete type values has its own required fields, validated by the emitter (invalid-security-scheme on violation — the scheme is dropped, never emitted): userPassword, X509, symmetricEncryption, asymmetricEncryption, plain, scramSha256, scramSha512, and gssapi need nothing beyond type; apiKey needs in: "user" | "password"; httpApiKey needs name + in: "header" | "query" | "cookie"; http needs scheme (with optional bearerFormat when scheme is "bearer"); oauth2 needs a flows object (implicit/password/clientCredentials/authorizationCode, each with its own required-field combination of authorizationUrl/tokenUrl/availableScopes); openIdConnect needs openIdConnectUrl. A @serverSecurity/@security reference to an unregistered (or invalid) scheme name reports unknown-security-scheme; a @serverSecurity naming an undeclared server reports unknown-server.

@service(#{ title: "Order Events API" })
@server("main", "broker.example.com:9092", "kafka-secure")
@securityScheme("saslScram", #{ type: "scramSha256", description: "SASL/SCRAM-SHA-256 auth" })
@serverSecurity("main", #["saslScram"])
namespace OrderEvents {
  model OrderPlaced {
    @header @correlationId orderId: string;
    customerId: string;
  }

  @channel("orders.lifecycle")
  @publish
  op publishOrderPlaced(payload: OrderPlaced): void;
}

examples/kafka-orders/main.tsp uses exactly this pattern (Kafka bindings and security are orthogonal: protocol: "kafka-secure" still gets the @Kafka.schemaRegistry server binding) — see test/kafka-orders.golden.json for the emitted security ref array and securitySchemes entry.

Versioned specs

Install @typespec/versioning (an optional peer dependency -- the emitter works fine without it) and @versioned your service namespace; the emitter then produces one AsyncAPI document per version instead of one document overall:

npm install @typespec/versioning
import "typespec-asyncapi";
import "@typespec/versioning";
using AsyncAPI;
using Versioning;

@versioned(Versions)
@service(#{ title: "Order Events API" })
namespace OrderEvents {
  enum Versions {
    v1,
    v2,
  }

  model OrderPlaced {
    orderId: string;

    @added(Versions.v2)
    priority?: string;
  }

  @channel("orders.lifecycle")
  @publish
  op publishOrderPlaced(payload: OrderPlaced): void;
}

This compiles to asyncapi.v1.json and asyncapi.v2.json -- each a complete, independently valid AsyncAPI document scoped to that version's view of the service (so v1's OrderPlaced schema has no priority property, and any operation/model/field added or removed at a later version only shows up in the documents for the versions where it exists). Unversioned services are unaffected and still emit a single asyncapi.json.

The output-file emitter option controls the file name (relative to the emitter output dir) and supports a {version} interpolation token -- e.g. "output-file": "specs/{version}/asyncapi.json" in tspconfig.yaml emitter options. Left unset, the default is asyncapi.json when unversioned and asyncapi.{version}.json per version otherwise.

Validate & render

Every document this emitter produces is asserted against the official @asyncapi/parser with zero errors as part of the test suite — but you can check any output yourself, or feed it into the rest of the AsyncAPI toolchain:

npx @asyncapi/cli validate path/to/asyncapi.json

Or drop the file into AsyncAPI Studio to render interactive docs, or run it through @asyncapi/generator/Modelina for codegen.

Generate code from the emitted spec

examples/codegen/ has two verified, runnable pipelines built on the doc above: Kotlin/TypeScript model classes via Modelina, and a Node.js service scaffold (routes + handlers) via @asyncapi/generator. It's a standalone package with its own quick-start, plus a "what doesn't work and why" section covering the 3.0→2.x downconversion dead-end and other version-gated dead paths.

Design principles

  • AsyncAPI 3.0 first. No 2.x emission — and no downconversion path exists in the ecosystem (converter-js only upgrades), so tooling that caps at 2.x is documented as incompatible rather than papered over (see examples/codegen).
  • Validated output, always. Every emitted document must pass the official @asyncapi/parser with zero errors — enforced as a test gate in CI.
  • Object-based emission. Documents are built as typed objects and serialized; never assembled by string concatenation.
  • Stable TypeSpec APIs only. Built on $onEmit + the compiler's public API (the same surface as @typespec/openapi3), not the experimental emitter framework.

Development

npm install
npm run build
npm test

License

MIT