asyncapi-jsdoc
v2.0.0
Published
Generates AsyncAPI specifications from JSDoc comments and serves interactive UI via Express.
Maintainers
Readme
asyncapi-jsdoc
A code-first approach to event-driven API documentation for Node.js, mirroring the popular swagger-jsdoc developer experience but tailored for AsyncAPI v3.0.0.
asyncapi-jsdoc parses JSDoc comments containing @asyncapi tags, extracts YAML content, validates the schema using the official @asyncapi/parser, resolves $ref references so components can be shared across channels/files, and serves the interactive web UI via an Express middleware.
🛠️ Features
- Code-First documentation: Define event-driven architectures directly alongside event handlers or controllers.
- AsyncAPI v3.0.0 ready: Decouples channels and operations natively.
- Robust YAML parsing: Preserves space margins for indentation-sensitive YAML by cleaning JSDoc leading asterisks line-by-line.
- Official Schema Validation: Integrated with
@asyncapi/parser(version 3.x) to ensure compliance before rendering. - Automatic
$refResolution: Share a schema or message across multiple channels/JSDoc blocks via$refandcomponents.schemas/components.messages— the spec returned bygenerateAsyncAPISpecalways comes back fully dereferenced (inlined), so downstream consumers (JSON Schema validators, the UI, custom tooling) never see a raw pointer. - Precise Error Tracking: Pinpoints the exact file and line number of any malformed YAML JSDoc comment.
- Interactive UI Middleware: Renders a gorgeous, dark-themed, high-contrast UI (powered by
@asyncapi/react-componentstandalone bundle via CDN).
📦 Installation
npm install asyncapi-jsdocEnsure you have express installed if you intend to serve the interactive web UI:
npm install express📝 JSDoc @asyncapi Comment Structure (v3.0.0)
Under AsyncAPI v3.0.0, channels and operations are decoupled. The channel defines the physical endpoint and messages, while the operation defines the action (send or receive).
[!IMPORTANT] Because channel names typically contain slashes (e.g.
user/signup), referencing them underoperationsrequires using the escaped JSON Pointer representation~1(e.g.#/channels/user~1signup).
Here is a visual template of how JSDoc comment blocks should be structured:
/**
* @asyncapi
* channels:
* user/signup:
* address: user.signup
* messages:
* signupMessage:
* $ref: '#/components/messages/SignupMessage'
* operations:
* userSignup:
* action: receive
* channel:
* $ref: '#/channels/user~1signup'
* components:
* messages:
* SignupMessage:
* summary: Event triggered when a new user registers in the system.
* payload:
* type: object
* properties:
* id:
* type: string
* format: uuid
* email:
* type: string
* format: email
*/
export function handleUserSignup(data: any) {
// Your logic here...
}[!NOTE] The
$ref: '#/components/messages/SignupMessage'above is not just cosmetic —generateAsyncAPISpecresolves it. The value returned to your app haschannels.user/signup.messages.signupMessagefully inlined with the actualSignupMessagepayload, not the pointer. This is how you reuse a schema or message across multiple channels, or even multiple JSDoc blocks/files: define it once undercomponents.schemas/components.messages(in any file scanned byapis), then$refit from as many places as you like — the merge across files happens before resolution, so it doesn't matter which file defines the component and which file references it. Setdereference: falseif you need the raw, unresolved shape instead.
🚀 Quick Start Example
Here is a complete setup using Express and TypeScript:
1. File: events.ts
/**
* @asyncapi
* channels:
* orders/created:
* address: orders.created
* messages:
* orderCreatedMessage:
* $ref: '#/components/messages/OrderCreatedMessage'
* operations:
* orderCreated:
* action: send
* channel:
* $ref: '#/channels/orders~1created'
* components:
* messages:
* OrderCreatedMessage:
* summary: Publishes an event when a new order is created.
* payload:
* type: object
* properties:
* orderId:
* type: string
* amount:
* type: number
*/
export function emitOrderCreated() {
console.log('Order created event emitted!');
}2. File: server.ts
import express from 'express';
import { generateAsyncAPISpec, serveAsyncApi } from 'asyncapi-jsdoc';
const app = express();
const port = 3000;
async function startServer() {
try {
// 1. Generate and validate the AsyncAPI spec from JSDoc
const spec = await generateAsyncAPISpec({
definition: {
asyncapi: '3.0.0',
info: {
title: 'My Event API',
version: '1.0.0',
description: 'Microservices and event messaging documentation',
},
servers: {
rabbitmq: {
host: 'localhost:5672',
protocol: 'amqp',
},
},
},
apis: ['./events.ts'],
validate: true, // Performs schema validation under the hood
});
// 2. Mount the UI and JSON Router middleware
app.use(serveAsyncApi(spec, { path: '/asyncapi' }));
// 3. Optional health path
app.get('/', (req, res) => {
res.send('Server active. View docs at <a href="/asyncapi">/asyncapi</a>');
});
app.listen(port, () => {
console.log(`🚀 Test server running successfully!`);
console.log(`📄 Spec JSON: http://localhost:${port}/asyncapi.json`);
console.log(`🎨 Interactive UI: http://localhost:${port}/asyncapi`);
});
} catch (error: any) {
console.error('❌ Critical error starting server:', error.message);
process.exit(1);
}
}
startServer();⚙️ API Configuration Specifications
generateAsyncAPISpec(options)
Scans your codebase for @asyncapi tags, aggregates channels/components, and validates schema parameters.
options(Object):definition(Record<string, any>): Baseline AsyncAPI specification (must containasyncapiandinfo).apis(string[]): Array of glob patterns defining target file sources. Required — at least one pattern must match at least one file, orgenerateAsyncAPISpecthrows. There is no "static spec only" mode; the JSDoc scan is always mandatory.validate(boolean, optional): Triggers@asyncapi/parserschema validation when true. Defaults totrue.dereference(boolean, optional): Resolves/inlines every local$ref(#/...) in the merged spec before returning it, socomponents.schemas/components.messagesreused via$refare fully expanded wherever they're referenced. Defaults totrue. Set tofalseto get back the raw merged spec with$refpointers untouched (the pre-1.1.0 behavior). A$refchain that loops back on itself (e.g. a schema referencing itself, directly or through another schema) throws a descriptive error identifying the cycle, since it cannot be represented as inlined JSON.
serveAsyncApi(spec, options)
Delivers Express routes containing the raw JSON spec and the interactive react documentation dashboard.
spec(Record<string, any>): Compiled JSON specification.options(Object, optional):path(string, optional): Base route path. Defaults to/asyncapi. Serves the UI at<path>and raw JSON at<path>.json.cssUrl(string, optional): External CDN path override for@asyncapi/react-componentCSS styling.scriptUrl(string, optional): External CDN path override for@asyncapi/react-componentJS standalone bundle.
🧪 Development & Testing
Run local tests (utilizes the native Node.js test runner against the compiled dist/):
npm run build
npm run testBuild the library:
npm run buildLint and format:
npm run lint # ESLint (airbnb-base + TypeScript)
npm run lint:fix # auto-fix what ESLint can
npm run format # Prettier --writeCommit conventions
This repo uses Husky git hooks:
- pre-commit: runs
npm run build && npm test, thenlint-staged(ESLint + Prettier on staged files). - commit-msg: validates the message against Conventional Commits via commitlint (e.g.
fix: ...,feat: ...,chore: ...).
Commits that don't follow this format, or that fail lint/tests, are rejected locally before they're created.
📄 License
MIT
