Decouple your application with decorator-based events, HazelJS-native.
Event-driven architecture is one of the most effective ways to keep your codebase flexible and maintainable. When an order is created, you might need to send a confirmation email, update analytics, notify inventory, and log the action. Wiring all of that with direct method calls creates a tangled web of dependencies. The @hazeljs/event-emitter package solves this by letting you emit events and listen for them with a clean, decorator-based API—similar to what NestJS developers know and love.
Why Event-Driven?
In a typical request handler, you might do something like:
// ❌ Tightly coupled - every new side effect requires editing this method
async createOrder(dto: CreateOrderDto) {
const order = await this.orderRepo.save(dto);
await this.emailService.sendConfirmation(order);
await this.analyticsService.track('order_created', order);
await this.inventoryService.reserve(order.items);
return order;
}
With events, the same flow becomes:
// ✅ Decoupled - emitters don't know who's listening
async createOrder(dto: CreateOrderDto) {
const order = await this.orderRepo.save(dto);
this.eventEmitter.emit('order.created', { orderId: order.id, order });
return order;
}
Now you can add new listeners (Slack notifications, audit logs, etc.) without touching the order service. Each listener is independent and can be tested in isolation.
Introducing @hazeljs/event-emitter
The @hazeljs/event-emitter package brings event-driven architecture to HazelJS with:
- @OnEvent() decorator — Declare event listeners on any method
- EventEmitterService — Inject and emit from anywhere in your app
-
Wildcard support — Listen to patterns like
order.*when enabled - Async listeners — Full support for async handlers
- DI integration — Works seamlessly with HazelJS dependency injection
Under the hood, it uses eventemitter2, the same library that powers NestJS's event emitter.
Installation
npm install @hazeljs/event-emitter
Quick Start
1. Import the module
import { HazelModule } from '@hazeljs/core';
import { EventEmitterModule } from '@hazeljs/event-emitter';
@HazelModule({
imports: [EventEmitterModule.forRoot()],
providers: [OrderService, OrderEventHandler],
})
export class AppModule {}
2. Emit events
Inject EventEmitterService and call emit():
import { Injectable } from '@hazeljs/core';
import { EventEmitterService } from '@hazeljs/event-emitter';
@Injectable()
export class OrderService {
constructor(private eventEmitter: EventEmitterService) {}
createOrder(order: Order) {
// ... save order
this.eventEmitter.emit('order.created', { orderId: order.id, order });
}
}
3. Listen with @OnEvent
Create an event handler class:
import { Injectable } from '@hazeljs/core';
import { OnEvent } from '@hazeljs/event-emitter';
@Injectable()
export class OrderEventHandler {
@OnEvent('order.created')
handleOrderCreated(payload: { orderId: string; order: Order }) {
console.log('Order created:', payload.orderId);
// Send email, update analytics, etc.
}
}
4. Register listeners
After your app bootstraps, register listeners from your providers:
import { EventEmitterModule } from '@hazeljs/event-emitter';
// Resolves from DI and registers all @OnEvent handlers
EventEmitterModule.registerListenersFromProviders([OrderEventHandler]);
Wildcards and Namespaces
Enable wildcards in forRoot() to listen to event patterns:
EventEmitterModule.forRoot({ wildcard: true })
Then you can do:
@OnEvent('order.*')
handleOrderEvents(payload: unknown) {
// Catches order.created, order.shipped, order.cancelled, etc.
}
Use ** for multi-level matching (e.g. order.delayed.out_of_stock).
Async Listeners
For async work, use the async option:
@OnEvent('order.created', { async: true })
async handleOrderCreated(payload: OrderCreatedEvent) {
await this.emailService.sendConfirmation(payload.order);
await this.analyticsService.track('order_created', payload);
}
By default, errors in listeners are suppressed (logged but not rethrown). Set suppressErrors: false if you want errors to propagate.
Configuration Options
EventEmitterModule.forRoot({
wildcard: true, // Enable order.* patterns
delimiter: '.', // Namespace delimiter
maxListeners: 10, // Max listeners per event
isGlobal: true, // Global module (default)
});
Real-World Example
Here's a complete flow: order creation triggers email, analytics, and logging—all decoupled.
// app.module.ts
@HazelModule({
imports: [EventEmitterModule.forRoot({ wildcard: true })],
controllers: [OrderController],
providers: [OrderService, OrderEventHandler, EmailService],
})
export class AppModule {}
// order.service.ts
@Injectable()
export class OrderService {
constructor(private eventEmitter: EventEmitterService) {}
async createOrder(dto: CreateOrderDto) {
const order = await this.saveOrder(dto);
this.eventEmitter.emit('order.created', { orderId: order.id, order });
return order;
}
}
// order-event.handler.ts
@Injectable()
export class OrderEventHandler {
constructor(private emailService: EmailService) {}
@OnEvent('order.created', { async: true })
async handleOrderCreated(payload: { orderId: string; order: Order }) {
await this.emailService.sendOrderConfirmation(payload.order);
}
@OnEvent('order.*')
logOrderEvent(payload: unknown) {
console.log('Order event:', payload);
}
}
// main.ts
const app = new HazelApp(AppModule);
await app.listen(3000);
EventEmitterModule.registerListenersFromProviders([OrderEventHandler]);
Best Practices
-
Use descriptive event names — Prefer
order.createdoverorderCreatefor namespacing. -
Register listeners early — Call
registerListenersFromProvidersright after app bootstrap. - Type your payloads — Define interfaces for event payloads for better type safety.
-
Keep handlers focused — Each
@OnEventhandler should do one thing; emit more events to chain logic. -
Use wildcards sparingly —
order.*is great for logging; avoid**unless you really need it.
Learn More
- Event Emitter Package Documentation — Full API reference and configuration
- HazelJS Documentation — Guides, packages, and API reference
- Cron Package — Schedule tasks that can emit events
- Kafka Package — Distributed event streaming for microservices
@hazeljs/event-emitter — Event-driven architecture, the HazelJS way.
Top comments (0)