Skip to content

Event-Manager

@voltage/event-manager is a type-safe event system for NestJS. Events are defined as classes, so emitting an event with the wrong arguments is a compile error, not a runtime surprise.

Terminal window
yarn add @voltage/event-manager

Import EventManagerModule once in your root application module. Place it first in the imports array — other modules may emit events during their own bootstrap, and the event manager must be initialized before them to catch those events:

import { EventManagerModule } from '@voltage/event-manager';
@Module({
imports: [
EventManagerModule.forRoot(),
// ... other modules
],
})
export class AppModule {}

Because the module is global, you do not need to import it in feature modules.

An event is a plain class with constructor parameters:

export class AfterOrderCreateEvent {
constructor(
public readonly order: Order,
public readonly user: User,
) {}
}

Inject EventEmitter and call emit() with the event class and its constructor arguments:

import { Injectable } from '@nestjs/common';
import { EventEmitter } from '@voltage/event-manager';
@Injectable()
export class OrderService {
constructor(private readonly emitter: EventEmitter) {}
async create(user: User, items: Item[]) {
const order = await this.repository.save({ user, items });
await this.emitter.emit(AfterOrderCreateEvent, order, user);
return order;
}
}

The emitter instantiates the event class for you. TypeScript infers the argument types from the constructor signature — pass the wrong types and it won’t compile.

Add @OnEvent() to any method in any provider, service, or module class:

import { Injectable } from '@nestjs/common';
import { OnEvent } from '@voltage/event-manager';
@Injectable()
export class NotificationService {
@OnEvent(AfterOrderCreateEvent)
protected async onOrderCreate(event: AfterOrderCreateEvent) {
await this.sendConfirmation(event.user, event.order);
}
}

Listeners are discovered automatically at application bootstrap — no manual registration required. The method can be public, protected, or private.

Module classes are valid listeners too, which is useful when a module needs to react to events from other modules without introducing a dedicated service:

import { Module } from '@nestjs/common';
import { OnEvent } from '@voltage/event-manager';
@Module({
controllers: [AuthController],
})
export class AuthModule {
constructor(private readonly url: URLGenerator) {}
@OnEvent(NavEvent)
protected onNavEvent(event: NavEvent) {
event.actions.push({
label: 'Logout',
url: this.url.generate(AuthController, 'logout'),
});
}
}

Handlers execute sequentially in registration order, each awaited before the next runs. This means handlers can mutate the event object and later handlers will see those changes:

export class NavEvent {
actions: NavAction[] = [];
}
// Handler A runs first
@OnEvent(NavEvent)
protected onNav(event: NavEvent) {
event.actions.push({ label: 'Profile', url: this.url.generate(ProfileController) });
}
// Handler B sees Handler A's additions
@OnEvent(NavEvent)
protected onNavAuth(event: NavEvent) {
event.actions.push({ label: 'Logout', url: this.url.generate(AuthController, 'logout') });
}

Handler return values are discarded. Mutation is the only way to communicate back through the event.

emit() throws on the first handler error, stopping execution of subsequent handlers:

await this.emitter.emit(AfterOrderCreateEvent, order, user);

softEmit() logs errors and continues to the next handler:

await this.emitter.softEmit(AfterOrderCreateEvent, order, user);

Use softEmit() when one handler failing should not affect others. Use emit() when handler failures are critical and should propagate.

EventManagerModule automatically emits two lifecycle events you can subscribe to:

import { ApplicationBootstrapEvent, ApplicationShutdownEvent } from '@voltage/event-manager';
@Injectable()
export class CacheService {
@OnEvent(ApplicationBootstrapEvent)
protected async onBootstrap() {
await this.warmUp();
}
@OnEvent(ApplicationShutdownEvent)
protected async onShutdown() {
await this.flush();
}
}