EventsTechniques · NestJS

Decouple what happens after an adoption from the code that adopts: CatsService emits namespaced events with typed payloads, and an audit module that never imports it listens, with a wildcard for the whole namespace.

What you will learn

Read the theory for Events

All Techniques lessons

All NestJS courses

loading types…

What you'll learn

  • Register EventEmitterModule and emit namespaced events with class payloads from a service
  • Listen with @OnEvent on any provider, including a wildcard over a namespace
  • Explain what events buy over direct calls, and when a listener runs relative to the response

Events

When a cat is adopted, several things should happen: the register gets a line, the statistics change, someone gets an email, the lobby screen updates. The obvious code puts all of that in CatsService.adopt(), and CatsService ends up knowing about email, screens and statistics, importing their modules, and breaking whenever one of them changes. Events cut those threads. The service announces that a cat was adopted and forgets about it; anyone interested listens. The two sides never import each other, and a new listener is a new file, not a change to the service.

EventEmitterModule

@nestjs/event-emitter wraps the eventemitter2 library, an event emitter with namespaces and wildcards. Register it once in the root module:

@Module({
  imports: [EventEmitterModule.forRoot()],
})
export class AppModule {}

forRoot() takes the emitter's options: wildcard: true enables patterns like order.*, delimiter is the namespace separator (a dot by default), maxListeners and verboseMemoryLeak guard against a listener leak, ignoreErrors decides whether an event with no listener and an error name throws.

Emitting

Inject EventEmitter2 and call emit with a name and a payload:

this.eventEmitter.emit(
  'order.created',
  new OrderCreatedEvent({ orderId: 1, payload: {} }),
);

Two conventions worth keeping. Names are namespaced, order.created, cat.adopted, so listeners can subscribe to a whole namespace and a log reads well. Payloads are class instances, small and immutable, so a listener knows what it receives and a rename shows up in the type checker. emit returns after every listener ran synchronously; emitAsync returns a promise of every listener's result, for the cases where the emitter must wait.

Listening

A listener is a method on any provider, decorated with the event it wants:

@OnEvent('order.created')
handleOrderCreatedEvent(payload: OrderCreatedEvent) {
  // handle and process "OrderCreatedEvent" event
}

Nest scans the providers when the application starts and registers every @OnEvent method with the emitter. The options: { async: true } runs the listener after the emitter returns, so a slow listener does not delay the request; { suppressErrors: false } lets a listener's exception reach the emitter, which by default swallows it, because a listener that throws should not break the code that emitted. With wildcards enabled, @OnEvent('order.*') hears every event in the namespace and @OnEvent('**') every event at all, which is how an audit log or a metrics counter subscribes once.

One timing rule: listeners are registered during onApplicationBootstrap. An event emitted earlier, from a constructor or onModuleInit, is lost. EventEmitterReadinessWatcher.waitUntilReady() waits for the registration when a module must emit during startup.

Where it sits

Events are not part of the request pipeline; they happen inside the handler's work, wherever emit is called. A synchronous listener runs before the handler's response is sent, an async: true listener after. Neither can change the response, and a failing listener does not fail the request unless you ask for that.

Your task

The cats module announces; the audit module, which knows nothing about cats, listens.

  1. AppModule registers the event emitter with wildcards enabled.
  2. CatsService emits cat.created with a CatCreatedEvent after a cat is saved, and cat.adopted with a CatAdoptedEvent after an adoption.
  3. AuditService listens: cat.created records an arrival, cat.adopted records the adoption and counts it, and a wildcard listener on cat.* counts every event.

Run after step 3 alone and adopt a cat: the listeners exist and nothing is announced, so the audit stays empty. Announcing and listening are two halves of one contract.

When it fails

  • Nest can't resolve dependencies of the CatsService (CatRepository, ?), argument EventEmitter2: EventEmitterModule.forRoot() is not imported.
  • Listeners never run: the event name differs between emit and @OnEvent, down to the namespace; or the listener's class is not a provider, so Nest never scanned it.
  • cat.* hears nothing while cat.created does: wildcards are off; pass { wildcard: true } to forRoot().
  • The audit is one behind: the listener is async: true and the audit was read before it ran.

Remember

  • EventEmitterModule.forRoot({ wildcard: true }) once; EventEmitter2 injected where events are emitted.
  • emit('namespace.name', new SomeEvent(...)): a namespaced name and a typed payload.
  • @OnEvent('name') on a provider's method; 'namespace.*' and '**' with wildcards; async and suppressErrors as options.
  • Emitter and listeners never import each other; that is the point.
Stuck? Show a hint

app.module.ts: EventEmitterModule.forRoot({ wildcard: true }) from '@nestjs/event-emitter'. Service: constructor(..., private readonly events: EventEmitter2) {} then this.events.emit('cat.created', new CatCreatedEvent(cat.id, cat.name)) and this.events.emit('cat.adopted', new CatAdoptedEvent(saved.id, saved.name, by)). Audit: @OnEvent('cat.created'), @OnEvent('cat.adopted') and @OnEvent('cat.*') on the three methods.