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.

Use emitSync() when you cannot await — for example inside Express middleware where async handlers are not supported. All handlers must be synchronous; the emitter throws if any handler returns a Promise:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { EventEmitter } from '@voltage/event-manager';
@Injectable()
export class AccessMiddleware implements NestMiddleware {
constructor(private readonly emitter: EventEmitter) {}
use(req: Request, res: Response, next: NextFunction) {
const event = this.emitter.emitSync(AccessCheckEvent, req.path);
if (!event.allowed) {
res.status(403).end();
return;
}
next();
}
}

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.

When you cannot reference the event class statically — for example, when the event comes from an optional package that may not be installed — use on() to register a handler function directly:

import { ApplicationBootstrapEvent, EventEmitter, OnEvent } from '@voltage/event-manager';
@Injectable()
export class IntegrationService {
constructor(private readonly emitter: EventEmitter) {}
@OnEvent(ApplicationBootstrapEvent)
protected async onBootstrap() {
try {
const { SomeEvent } = await import('@vendor/optional-package');
this.emitter.on(SomeEvent, (event) => {
// handle event
});
} catch {
// @vendor/optional-package is not installed
}
}
}

on() returns an unsubscribe function:

const off = this.emitter.on(SomeEvent, this.onSomeEvent.bind(this));
// later...
off();

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();
}
}