@juice10/hono-zod-asyncapi
v0.1.0
Published
A Hono package for generating AsyncAPI specifications for WebSocket routes with Zod validation
Maintainers
Readme
@juice10/hono-zod-asyncapi
A Hono package for generating AsyncAPI specifications for WebSocket routes with Zod validation.
Features
- 🚀 AsyncAPI 3.0 Support - Generate AsyncAPI 3.0 compliant specifications
- 🔌 WebSocket Integration - Built specifically for WebSocket channels
- ✅ Zod Validation - Use Zod schemas for message validation and type safety
- 📝 Type Safety - Full TypeScript support with automatic type inference
- 🎯 Easy to Use - Simple API similar to
@hono/zod-openapi - 📚 Documentation Generation - Auto-generate API documentation from your code
- 🔄 OpenAPI Integration - Works seamlessly with
@hono/zod-openapifor unified REST + WebSocket APIs
Installation
npm install @juice10/hono-zod-asyncapi hono zodQuick Start
import { AsyncAPIHono, createChannel, z } from '@juice10/hono-zod-asyncapi'
// Create an AsyncAPI-enabled Hono app
const app = new AsyncAPIHono({
info: {
title: 'Chat API',
version: '1.0.0',
description: 'Real-time chat API using WebSockets',
},
servers: {
development: {
host: 'localhost:3000',
protocol: 'ws',
description: 'Development server',
},
},
})
// Define a channel with Zod schemas
const chatChannel = createChannel({
path: '/chat/:roomId',
description: 'Real-time chat room',
send: {
payload: z.object({
message: z.string(),
userId: z.string(),
timestamp: z.number(),
}),
description: 'Send a chat message to the room',
},
receive: {
payload: z.object({
message: z.string(),
userId: z.string(),
timestamp: z.number(),
}),
description: 'Receive chat messages from the room',
},
tags: ['chat'],
})
// Register the channel with a handler
app.channel('chatRoom', chatChannel, (ws, message, ctx) => {
console.log(`Message in room ${ctx.params.roomId}:`, message)
// Send a response
ws.send({
message: `Echo: ${message.message}`,
userId: 'system',
timestamp: Date.now(),
})
})
// Serve the AsyncAPI documentation
app.doc('/asyncapi.json')
export default appIntegration with @hono/zod-openapi
Combine REST APIs (OpenAPI) and WebSocket APIs (AsyncAPI) in a single application:
npm install @juice10/hono-zod-asyncapi @hono/zod-openapi hono zodOption 1: Merge Separate Apps
import { OpenAPIHono, createRoute, z as openAPIZ } from '@hono/zod-openapi'
import { AsyncAPIHono, createChannel, z, mergeAPIDocs } from '@juice10/hono-zod-asyncapi'
// Create REST API with OpenAPI
const restAPI = new OpenAPIHono()
const getUserRoute = createRoute({
method: 'get',
path: '/api/users/:id',
responses: {
200: {
content: {
'application/json': {
schema: openAPIZ.object({
id: openAPIZ.string(),
name: openAPIZ.string(),
}),
},
},
description: 'User details',
},
},
})
restAPI.openapi(getUserRoute, (c) => {
return c.json({ id: '123', name: 'Alice' })
})
// Create WebSocket API with AsyncAPI
const wsAPI = new AsyncAPIHono({
info: {
title: 'WebSocket API',
version: '1.0.0',
},
})
const chatChannel = createChannel({
path: '/ws/chat/:roomId',
send: {
payload: z.object({
message: z.string(),
userId: z.string(),
}),
},
})
wsAPI.channel('chat', chatChannel, (ws, message, ctx) => {
console.log(`Chat message in room ${ctx.params.roomId}`)
ws.send({ message: 'Hello!', userId: 'system' })
})
// Merge both APIs
const app = mergeAPIDocs(restAPI, wsAPI)
// Serve both documentations
app.doc('/api/openapi.json') // OpenAPI spec for REST
app.asyncapiDoc('/api/asyncapi.json') // AsyncAPI spec for WebSocket
export default appOption 2: Unified API App
import { UnifiedAPIHono, createChannel, z } from '@juice10/hono-zod-asyncapi'
const app = new UnifiedAPIHono({
openapi: {
info: { title: 'My API', version: '1.0.0' },
},
asyncapi: {
info: { title: 'My WebSocket API', version: '1.0.0' },
},
})
// Add WebSocket channels
const channel = createChannel({
path: '/ws/events',
send: { payload: z.object({ event: z.string() }) },
})
app.channel('events', channel)
// Serve both docs
app.docs('/api/openapi.json', '/api/asyncapi.json')Unified Configuration Helpers
import { createUnifiedInfo, createUnifiedServers } from '@juice10/hono-zod-asyncapi'
// Create consistent info for both specs
const info = createUnifiedInfo({
title: 'My API',
version: '1.0.0',
description: 'REST and WebSocket API',
contact: {
name: 'API Team',
email: '[email protected]',
},
})
// Create server configs for both HTTP and WebSocket
const servers = createUnifiedServers({
http: {
url: 'https://api.example.com',
description: 'Production HTTP server',
},
ws: {
host: 'ws.example.com',
protocol: 'wss',
description: 'Production WebSocket server',
},
})
// Use in your apps
const restAPI = new OpenAPIHono({ openapi: { ...info.openapi, servers: servers.openapi } })
const wsAPI = new AsyncAPIHono({ info: info.asyncapi, servers: servers.asyncapi })API Reference
AsyncAPIHono
The main class that extends Hono with AsyncAPI support.
const app = new AsyncAPIHono({
info: {
title: 'My API',
version: '1.0.0',
description: 'API description',
},
servers: {
production: {
host: 'api.example.com',
protocol: 'wss',
description: 'Production server',
},
},
})Methods
channel(id, config, handler?)- Register a WebSocket channeldoc(path, options?)- Serve AsyncAPI document as JSONdocYAML(path, options?)- Serve AsyncAPI document as YAMLgetAsyncAPIDocument(options?)- Get the AsyncAPI document objectroute(path, app)- Merge routes from another AsyncAPIHono instancebasePath(path)- Create a new instance with a base path
createChannel
Factory function for creating type-safe channel configurations.
const channel = createChannel({
path: '/events/:eventId',
description: 'Event stream',
send: {
payload: z.object({ type: z.string(), data: z.any() }),
description: 'Send event data',
},
receive: {
payload: z.object({ type: z.string(), data: z.any() }),
description: 'Receive event updates',
},
parameters: {
eventId: z.string().uuid(),
},
tags: ['events'],
})Channel Handler
Type-safe handler for WebSocket messages:
type ChannelHandler = (
ws: WebSocketData,
message: InferredMessagePayload,
context: ChannelContext
) => void | Promise<void>WebSocketData
send(data)- Send a message (validated against send schema)close(code?, reason?)- Close the WebSocket connection
ChannelContext
params- Path parameters (typed based on channel path)headers- Request headers
Examples
Basic WebSocket Channel
import { AsyncAPIHono, createChannel, z } from '@juice10/hono-zod-asyncapi'
const app = new AsyncAPIHono()
const pingChannel = createChannel({
path: '/ping',
description: 'Ping-pong channel',
receive: {
payload: z.object({ ping: z.string() }),
},
send: {
payload: z.object({ pong: z.string() }),
},
})
app.channel('ping', pingChannel, (ws, message) => {
ws.send({ pong: message.ping })
})Channel with Path Parameters
const userChannel = createChannel({
path: '/users/:userId/notifications',
description: 'User-specific notification channel',
send: {
payload: z.object({
type: z.enum(['info', 'warning', 'error']),
message: z.string(),
}),
},
})
app.channel('userNotifications', userChannel, (ws, message, ctx) => {
console.log(`Notification for user ${ctx.params.userId}`)
// Handler logic
})Complex Message Schemas
const gameChannel = createChannel({
path: '/game/:gameId',
description: 'Real-time game state updates',
send: {
payload: z.discriminatedUnion('type', [
z.object({
type: z.literal('move'),
playerId: z.string(),
position: z.object({ x: z.number(), y: z.number() }),
}),
z.object({
type: z.literal('chat'),
playerId: z.string(),
message: z.string(),
}),
]),
},
receive: {
payload: z.object({
gameState: z.object({
players: z.array(z.any()),
status: z.enum(['waiting', 'playing', 'finished']),
}),
}),
},
})Modular Architecture
// channels/chat.ts
export const chatRoutes = new AsyncAPIHono()
chatRoutes.channel('publicChat', publicChatChannel, handler1)
chatRoutes.channel('privateChat', privateChatChannel, handler2)
// channels/notifications.ts
export const notificationRoutes = new AsyncAPIHono()
notificationRoutes.channel('userNotifications', notificationChannel, handler)
// main.ts
const app = new AsyncAPIHono({
info: {
title: 'My API',
version: '1.0.0',
},
})
app.route('/chat', chatRoutes)
app.route('/notifications', notificationRoutes)
app.doc('/asyncapi.json')Custom Server Configuration
import { createWebSocketServer, createSecureWebSocketServer } from '@juice10/hono-zod-asyncapi'
const app = new AsyncAPIHono({
info: {
title: 'Multi-Environment API',
version: '1.0.0',
},
servers: {
development: createWebSocketServer('localhost:3000', 'Development server'),
staging: createSecureWebSocketServer('staging.api.example.com', 'Staging server'),
production: createSecureWebSocketServer('api.example.com', 'Production server'),
},
})AsyncAPI Specification
This package generates AsyncAPI 3.0 compliant specifications. The generated document includes:
- Channels - WebSocket endpoints with their addresses
- Operations - Send and receive operations for each channel
- Messages - Message schemas with Zod-validated payloads
- Components - Reusable schemas and message definitions
Example Generated Spec
{
"asyncapi": "3.0.0",
"info": {
"title": "Chat API",
"version": "1.0.0"
},
"servers": {
"development": {
"host": "localhost:3000",
"protocol": "ws"
}
},
"channels": {
"chatRoom": {
"address": "/chat/{roomId}",
"messages": {
"chatRoom_send_message": {
"$ref": "#/components/messages/chatRoom_send_message"
}
}
}
},
"operations": {
"send_chatRoom_0": {
"action": "send",
"channel": {
"$ref": "#/channels/chatRoom"
},
"messages": [
{
"$ref": "#/components/messages/chatRoom_send_message"
}
]
}
},
"components": {
"messages": {
"chatRoom_send_message": {
"payload": {
"$ref": "#/components/schemas/schema_0"
}
}
},
"schemas": {
"schema_0": {
"type": "object",
"properties": {
"message": { "type": "string" },
"userId": { "type": "string" }
},
"required": ["message", "userId"]
}
}
}
}TypeScript Support
Full TypeScript support with automatic type inference:
// Types are automatically inferred from Zod schemas
const channel = createChannel({
path: '/data',
send: {
payload: z.object({
value: z.number(),
label: z.string(),
}),
},
})
app.channel('data', channel, (ws, message, ctx) => {
// message is typed as { value: number; label: string }
const num: number = message.value
const str: string = message.label
// ws.send() expects the correct type
ws.send({ value: 42, label: 'Answer' }) // ✓ Valid
ws.send({ value: '42', label: 'Answer' }) // ✗ Type error
})Runtime Support
This package is designed to work with any JavaScript runtime that supports Hono. However, actual WebSocket handling depends on the runtime:
- Bun - Native WebSocket support
- Cloudflare Workers - WebSocket support via Durable Objects
- Node.js - Requires WebSocket library (ws, uWebSockets.js, etc.)
- Deno - Native WebSocket support
License
MIT
Credits
Inspired by @hono/zod-openapi by the Hono team.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
