AsyncAPI - Simplifying Channels Into One
Building integrations with event-driven services requires managing connections alongside your messaging logic. In our previous post on protocol-specific messaging functions, we explored how to generate low-level channel functions for NATS, Kafka, and other protocols. Now let's see how The Codegen Project's client generator takes this a step further by providing a convenient wrapper around those channel functions that handles connection management and exposes all relevant models like parameters and headers.
From Channel Functions to Client Wrapperโ
While channel functions provide excellent protocol-specific abstractions, they require you to manage connections manually and import various model types separately. The client generator creates a simple wrapper that:
- Connection Management - Provides connection handling methods on a class
- Centralized Access - All protocol functions accessible through one client instance
- Model Exposure - All relevant models (parameters, headers, payloads) are exported from one place
- Same Functionality - All the same protocol functions, just wrapped in a class
The client generator builds on the foundation of channel functions without changing their core functionality.
The Problem: Manual Connection Managementโ
When using raw channel functions (as generated by the channels preset), you typically write code like this:
// Using channel functions - manual connection management
import { Protocols } from './generated';
import { connect } from 'nats';
const { nats } = Protocols;
// Manual connection management
const connection = await connect({ servers: 'nats://localhost:4222' });
// Protocol-specific function calls
await nats.publishToPublishOrderCreated({
message: orderCreatedMessage,
parameters: orderCreatedParams,
nc: connection // Must pass connection to every call
});
// Must manage connection lifecycle yourself
await connection.close();
You need to manage connections manually and pass them to every function call.
The Solution: Client Wrapper + Connection Managementโ
The Codegen Project's client
preset generates a simple wrapper class that manages connections for you while exposing the same protocol functions.
๐ก Built on Channel Functions: The client generator automatically includes and wraps the channels generator functions, providing connection management while maintaining all the same functionality.
Real-World Example: E-commerce Order Management Clientโ
Let's build a client wrapper for order lifecycle management. We'll use the same AsyncAPI specification from our channels generator post, but generate a client wrapper that manages connections.
๐ก Complete Example: You can find the full working example, including all files mentioned in this post, in our ecommerce-asyncapi-client example.
Show me the AsyncAPI document!
asyncapi: 3.0.0
info:
title: E-commerce Order Lifecycle Events
version: 1.0.0
description: Event-driven order management system with comprehensive lifecycle tracking
channels:
# Order Management Channels
order-lifecycle:
address: orders.{action}
parameters:
action:
$ref: '#/components/parameters/OrderAction'
messages:
OrderCreated:
$ref: '#/components/messages/OrderCreated'
OrderUpdated:
$ref: '#/components/messages/OrderUpdated'
OrderCancelled:
$ref: '#/components/messages/OrderCancelled'
operations:
# Order Management Operations
OrderCreated:
action: send
channel:
$ref: '#/channels/order-lifecycle'
messages:
- $ref: '#/channels/order-lifecycle/messages/OrderCreated'
OrderUpdated:
action: send
channel:
$ref: '#/channels/order-lifecycle'
messages:
- $ref: '#/channels/order-lifecycle/messages/OrderUpdated'
OrderCancelled:
action: send
channel:
$ref: '#/channels/order-lifecycle'
messages:
- $ref: '#/channels/order-lifecycle/messages/OrderCancelled'
OrderEvents:
action: receive
channel:
$ref: '#/channels/order-lifecycle'
messages:
- $ref: '#/channels/order-lifecycle/messages/OrderCreated'
- $ref: '#/channels/order-lifecycle/messages/OrderUpdated'
- $ref: '#/channels/order-lifecycle/messages/OrderCancelled'
components:
# Reusable Parameters
parameters:
OrderAction:
enum: [created, updated, cancelled, shipped, delivered]
description: Order lifecycle action
# Reusable Messages
messages:
OrderCreated:
name: OrderCreated
title: Order Created Event
summary: Published when a new order is created
payload:
$ref: '#/components/schemas/OrderCreatedPayload'
headers:
$ref: '#/components/schemas/OrderHeaders'
OrderUpdated:
name: OrderUpdated
title: Order Updated Event
summary: Published when order details are modified
payload:
$ref: '#/components/schemas/OrderUpdatedPayload'
headers:
$ref: '#/components/schemas/OrderHeaders'
OrderCancelled:
name: OrderCancelled
title: Order Cancelled Event
summary: Published when an order is cancelled
payload:
$ref: '#/components/schemas/OrderCancelledPayload'
headers:
$ref: '#/components/schemas/OrderHeaders'
schemas:
# Order Payload Schemas
OrderCreatedPayload:
type: object
required: [orderId, customerId, items, totalAmount]
properties:
orderId:
type: string
format: uuid
customerId:
type: string
format: uuid
items:
type: array
items:
$ref: '#/components/schemas/OrderItem'
totalAmount:
$ref: '#/components/schemas/Money'
shippingAddress:
$ref: '#/components/schemas/Address'
createdAt:
type: string
format: date-time
OrderUpdatedPayload:
type: object
required: [orderId, status, updatedAt]
properties:
orderId:
type: string
format: uuid
status:
$ref: '#/components/schemas/OrderStatus'
updatedAt:
type: string
format: date-time
reason:
type: string
updatedFields:
type: array
items:
type: string
OrderStatus:
type: string
enum: [pending, confirmed, processing, shipped, delivered, cancelled]
OrderCancelledPayload:
type: object
required: [orderId, reason, cancelledAt]
properties:
orderId:
type: string
format: uuid
reason:
type: string
cancelledAt:
type: string
format: date-time
refundAmount:
$ref: '#/components/schemas/Money'
# Order Header Schema
OrderHeaders:
type: object
required: [x-correlation-id, x-order-id, x-customer-id]
properties:
x-correlation-id:
type: string
format: uuid
x-order-id:
type: string
format: uuid
x-customer-id:
type: string
format: uuid
x-source-service:
type: string
# Supporting Schemas
OrderItem:
type: object
required: [productId, quantity, unitPrice]
properties:
productId:
type: string
format: uuid
quantity:
type: integer
minimum: 1
unitPrice:
$ref: '#/components/schemas/Money'
productName:
type: string
productCategory:
type: string
Money:
type: object
required: [amount, currency]
properties:
amount:
type: integer
minimum: 0
description: Amount in smallest currency unit (e.g., cents for USD)
currency:
$ref: '#/components/schemas/Currency'
Currency:
type: string
enum: [USD, EUR, GBP]
Address:
type: object
required: [street, city, country, postalCode]
properties:
street:
type: string
city:
type: string
state:
type: string
country:
type: string
postalCode:
type: string
Generating the Client Wrapperโ
Create a configuration file to generate a TypeScript client wrapper:
// codegen.config.js
export default {
inputType: 'asyncapi',
inputPath: './ecommerce-event-channels.yaml',
generators: [
{
preset: 'client',
outputPath: './src/generated',
language: 'typescript',
protocols: ['nats']
}
]
};
Run the generator:
npx @the-codegen-project/cli generate codegen.config.js
This generates a wrapper class around the protocol functions:
// Generated: src/generated/NatsClient.ts
// All models exported for convenience
// export * from './payloads';
export class NatsClient {
private connection?: NatsConnection;
// Connection management methods
async connectToHost(url: string): Promise<void> {
this.connection = await connect({ servers: url });
}
async disconnect(): Promise<void> {
if (this.connection) {
await this.connection.close();
}
}
// Same protocol functions as channels generator, but wrapped
async publishToOrderCreated({message, parameters}: {
message: OrderCreated,
parameters: OrderLifecycleParameters
}): Promise<void> {
/* Handle connection logic and call channel function */
}
}
Using the Generated Client Wrapper: Connection Managementโ
Now let's see how the client wrapper simplifies connection management compared to raw channel functions:
Connection Management Made Simpleโ
import { NatsClient, OrderCreated, OrderLifecycleParameters } from './generated/NatsClient';
class OrderService {
private client = new NatsClient();
async initialize() {
// Connection managed by the client wrapper
await this.client.connectToHost('nats://localhost:4222');
console.log('โ
Connected to NATS server');
}
async createOrder(): Promise<string> {
const orderData = new OrderCreated({
orderId: '123e4567-e89b-12d3-a456-426614174000',
customerId: '987fcdeb-51a2-43d1-9f12-345678901234',
items: [/* order items */],
totalAmount: new Money({ amount: 309895, currency: Currency.USD }),
// ... other order data
});
const parameters = new OrderLifecycleParameters({ action: 'created' });
// Same function as channels generator, but connection is managed
await this.client.publishToOrderCreated({message: orderData, parameters});
console.log(`โ
Order created: ${orderData.orderId}`);
return orderData.orderId;
}
async cleanup() {
// Clean connection management
await this.client.disconnect();
}
}
Event Subscription with Managed Connectionโ
class OrderService {
private client = new NatsClient();
...
private async setupEventHandlers() {
const parameters = new OrderLifecycleParameters({ action: 'created' });
// Same callback signature as channel functions
await this.client.subscribeToOrderEvents({
onDataCallback: (err, message, parameters, natsMsg) => {
// Same callback parameters as channel functions
if (err) {
console.error('โ Error processing order event:', err.message);
return;
}
this.handleOrderEvent(message, parameters);
},
parameters
});
}
private handleOrderEvent(message: any, parameters?: OrderLifecycleParameters) {
const action = parameters?.action || 'unknown';
switch (action) {
case 'created':
console.log(`๐ฐ New sale: $${(message?.totalAmount?.amount / 100).toFixed(2)}`);
break;
case 'updated':
console.log(`๐ Status updated: ${message?.status}`);
break;
case 'cancelled':
console.log(`๐ธ Processing refund: $${(message?.refundAmount?.amount / 100).toFixed(2)}`);
break;
}
}
}
JetStream with Connection Managementโ
class DurableOrderProcessor {
private client = new NatsClient();
async setupDurableProcessing() {
const parameters = new OrderLifecycleParameters({ action: 'created' });
// Same JetStream functions as channel generator, but wrapped
await this.client.jetStreamPullSubscribeToOrderEvents({
onDataCallback: (err, message, parameters, jsMsg) => {
// Same callback as channel functions
if (err) {
console.error('โ Error processing order:', err.message);
return;
}
this.processOrderForFulfillment(message);
},
parameters,
options: { name: 'order-processor', config: { max_batch: 10 } }
});
}
private async processOrderForFulfillment(order: any) {
console.log(`๐ญ Processing order: ${order?.orderId}`);
// Business logic here...
// Same JetStream publish function, connection managed
const updateMessage = new OrderUpdated({
orderId: order?.orderId,
status: OrderStatus.PROCESSING,
updatedAt: new Date().toISOString(),
reason: 'Order processing started by fulfillment system'
});
const parameters = new OrderLifecycleParameters({ action: 'updated' });
await this.client.jetStreamPublishToOrderUpdated({message: updateMessage, parameters});
}
}
Client Wrapper vs Channel Functions: Key Differencesโ
Aspect | Channel Functions | Client Wrapper |
---|---|---|
Connection Management | Manual - you manage connections | Wrapped - client manages connections |
Function Signatures | Same protocol functions | Same protocol functions (wrapped) |
Method Names | Same as channel functions | Same as channel functions |
Callback Signature | Protocol-specific parameters | Same protocol-specific parameters |
Error Handling | Same protocol errors | Same protocol errors |
Model Access | Import separately | All exported from client |
Use Case | Direct protocol control | Connection convenience |
Benefits of This Approachโ
- Connection Management: Automatic connection handling in a class wrapper
- Centralized Models: All parameters, headers, and payloads exported from one place
- Same Functionality: All the same protocol functions, just connection-wrapped
- Type Safety: Same TypeScript support as channel functions
- Built on Channel Functions: Uses channel functions internally unchanged
- Protocol Functions: Same NATS/JetStream functions as channels generator
- Simple Wrapper: Minimal abstraction over existing channel functions
- Easy Migration: Can switch between channel functions and client wrapper easily
Conclusionโ
The client generator provides a thin wrapper around the channel functions from our channels generator, adding connection management convenience while preserving all the same protocol functionality.
This approach allows developers to use the same protocol functions they're familiar with, but with the convenience of managed connections and centralized model exports. The wrapper doesn't change the underlying functionality - it just makes connection management easier.
Ready to try it yourself? Check out the client generator documentation and start building your connection wrapper today!
Try It Yourselfโ
Want to see this in action? Clone our ecommerce-asyncapi-client example and run:
cd examples/ecommerce-asyncapi-client
npm install
npm run generate
npm run demo
This will generate the client wrapper and run the demonstration shown above, highlighting the connection management convenience with the same NATS messaging functions.
๐ฏ Key Benefits of Client Wrapper: โข Connection management in a class โข Centralized model exports โข Same protocol functions as channels generator โข Minimal wrapper overhead โข Easy migration path โข No functional changes to messaging
Additional Resourcesโ
Documentationโ
- Client Generator Documentation - Complete guide to client wrapper generation options and configuration
- Channels Generator - The protocol functions that client wrapper builds upon
- AsyncAPI Input Documentation - Understanding AsyncAPI specifications for code generation
- E-commerce Client Example - Complete working example from this blog post
- All Examples Repository - Browse all available examples and use cases
- Payload Generator - Generate type-safe payload models (auto-included in client)
- Headers Generator - Generate type-safe header models (auto-included in client)
- Types Generator - Generate type definitions (auto-included in client)
- NATS Protocol - NATS messaging client integration
- All Protocols - Browse all supported client protocols