DEV Community

Cover image for Event Emitters in NestJS Explained: Practical Scenarios and Best Practices
Dawit Girma
Dawit Girma

Posted on

Event Emitters in NestJS Explained: Practical Scenarios and Best Practices

Can I Make My System Faster?

Of course, you can. One practical way to improve application responsiveness is by using event emitters.

Imagine a user creates an account in your system. After account creation, your application may need to perform additional tasks such as:

  • Sending a welcome email
  • Triggering a notification queue
  • Logging audit activity
  • Updating analytics
  • Creating a profile record

If all of these tasks are executed before returning the API response, the user waits longer. This makes the system feel slower.

Instead, you can immediately return a success response after the main task is complete, then emit an event for background actions. Those actions can run independently.

Event emitters allow you to build applications using event-driven architecture.

In this architecture:

  • One part of the system emits an event after something happens
  • Other parts listen for that event and react
  • The emitter does not need to know who handles the event
  • Listeners do not need to know who emitted it

This creates loosely coupled systems, which are easier to maintain, test, and scale.

Common Application Areas

1 Notifications

  • Email notifications
  • SMS messages
  • Push notifications
  • In-app alerts

2 Logging and Audit Tracking

Track important user actions such as:

  • Login
  • Logout
  • Password change
  • Failed payment attempt
  • Role update

3 Background Processing

Useful for expensive tasks such as:

  • Compress uploaded images
  • Generate thumbnails
  • Convert video to HLS format
  • Export reports
  • Sync data with third-party APIs

4 Extensibility

You can add new listeners later without modifying the original code.

Example:

user.created

Initially triggers:

  • Welcome email

Later also triggers:

  • Referral reward system
  • CRM sync
  • Internal analytics

It is not recommended to use event emitters everywhere.

Use them when:

  • Tasks are independent
  • Background execution is acceptable
  • Loose coupling is valuable
  • Multiple modules react to one action

Use normal synchronous processing when:

  • Immediate result is required
  • Strong transaction consistency is required
  • The next step depends on previous completion

Prerequisites

Before following this article, you should know:

  • Basic knowledge of NestJS
  • Familiarity with backend workflows such as auth, orders, notifications
  • Basic understanding of event-driven architecture

Project Setup

We will create a minimal NestJS project to demonstrate event emitters.

1 Create New NestJS Project


nest new nest-event-emitter
Enter fullscreen mode Exit fullscreen mode

2 Install Event Emitter Package

npm i --save @nestjs/event-emitter
Enter fullscreen mode Exit fullscreen mode

3 Configure EventEmitterModule

Import it in app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
  imports: [EventEmitterModule.forRoot()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

This is the minimal configuration

Advanced Configuration

EventEmitterModule.forRoot({
  wildcard: false,
  delimiter: '.',
  newListener: false,
  removeListener: false,
  maxListeners: 10,
  verboseMemoryLeak: false,
  ignoreErrors: false,
});
Enter fullscreen mode Exit fullscreen mode

Configuration Explained

- wildcard

Allows pattern-based event names. If false, only exact names match.

@OnEvent('user.created')
Enter fullscreen mode Exit fullscreen mode

Only listens to:

user.created
Enter fullscreen mode Exit fullscreen mode

If true:

@OnEvent('user.*')
Enter fullscreen mode Exit fullscreen mode

Listens to:

user.created
user.deleted
user.updated
Enter fullscreen mode Exit fullscreen mode

- delimiter

Used with namespaces. Default commonly uses .

Examples:

user.created
order.shipped
payment.completed
Enter fullscreen mode Exit fullscreen mode

You may also use:

user:created
Enter fullscreen mode Exit fullscreen mode

if delimiter is :.

- newListener

Emits newListener whenever a listener is registered. Useful for debugging.

emitter.on('newListener', (event) => {
  console.log('Added listener for:', event);
});

emitter.on('user.created', () => {});
emitter.on('order.created', () => {});

Enter fullscreen mode Exit fullscreen mode

Output:

Added listener for: user.created
Added listener for: order.created
Enter fullscreen mode Exit fullscreen mode

- removeListener

Emits removeListener when a listener is removed.

emitter.on('removeListener', (event) => {
  console.log('Removed listener from:', event);
});

emitter.off('user.created', handler);
Enter fullscreen mode Exit fullscreen mode

Output:

Removed listener from: user.created
Enter fullscreen mode Exit fullscreen mode

Usually rarely needed.

- maxListeners

Warns when too many listeners are attached to one event.

maxListeners: 5
Enter fullscreen mode Exit fullscreen mode

This is not a hard limit. It warns about possible memory leaks.

- verboseMemoryLeak

Controls warning detail.

If false

Possible memory leak detected

Enter fullscreen mode Exit fullscreen mode

If true

Possible memory leak detected. Event name: user.created
Enter fullscreen mode Exit fullscreen mode

- ignoreErrors

Controls behavior of special error event.

emit('error', new Error('Payment gateway down'));
Enter fullscreen mode Exit fullscreen mode

If:

ignoreErrors: false
Enter fullscreen mode Exit fullscreen mode

Unhandled error events throw exceptions. Good during development.
If:

ignoreErrors: true

Enter fullscreen mode Exit fullscreen mode

Errors may be silently ignored.

4 Simple Event Example

  • Create Event Class : Store in events/user-created.event.ts
export class UserCreatedEvent {
  constructor(
    public readonly userId: number,
    public readonly email: string,
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

Using a class gives:

  • Strong typing
  • Structured payloads
  • Better IDE autocomplete
  • Easier future expansion

Instead of random objects.

  • Create Constants File: Avoid raw strings.
export const EVENTS = {
  USER_CREATED: 'user.created',
};
Enter fullscreen mode Exit fullscreen mode
  • Create Listener
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { EVENTS } from '../constants';
import { UserCreatedEvent } from '../events/user-created.event';

@Injectable()
export class UserCreatedListener {
  @OnEvent(EVENTS.USER_CREATED)
  handleUserCreatedEvent(event: UserCreatedEvent) {
    console.log(`User created event received: ${JSON.stringify(event)}`);

    console.log(
      `[Event Name: ${EVENTS.USER_CREATED}] User created with ID: ${event.userId} and Email: ${event.email}`,
    );
  }
}

Enter fullscreen mode Exit fullscreen mode
  • Emit Event
async createUser(userId: number, email: string) {
  console.log(`--- Mocking User Creation for ID: ${userId} ---`);
  const event = new UserCreatedEvent(userId, email);
  this.eventEmitter.emit(EVENTS.USER_CREATED, event);
}
Enter fullscreen mode Exit fullscreen mode
  • Call Method in main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AppService } from './app.service';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  await app.listen(process.env.PORT ?? 3000);

  const appService = app.get(AppService);

  await appService.createUser(1, 'mock.user@example.com');
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode
  • Register Listener
providers: [AppService, UserCreatedListener],
Enter fullscreen mode Exit fullscreen mode
  • Final log should look like

5 Wildcard Pattern Example

Now let us listen to all order-related events.

  • Enable Wildcards
EventEmitterModule.forRoot({
  wildcard: true,
  delimiter: '.',
}),

Enter fullscreen mode Exit fullscreen mode
  • Register Provider
providers: [AppService, UserCreatedListener, OrderListener],
Enter fullscreen mode Exit fullscreen mode
  • Create Order Events
export class OrderCreatedEvent {
  constructor(
    public readonly orderId: number,
    public readonly userId: number,
    public readonly total: number,
  ) {}
}

export class OrderDeliveredEvent {
  constructor(
    public readonly orderId: number,
    public readonly deliveredAt: Date,
  ) {}
}
Enter fullscreen mode Exit fullscreen mode
  • Add Constants
export const EVENTS = {
  USER_CREATED: 'user.created',
  ORDER_CREATED: 'order.created',
  ORDER_DELIVERED: 'order.delivered',
  ORDER_ALL: 'order.*',
};
Enter fullscreen mode Exit fullscreen mode
  • Create Wildcard Listener: I make it to wait to 10 seconds to separate from normal events.
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { EVENTS } from '../constants';
import { OrderCreatedEvent } from '../events/order-created.event';
import { OrderDeliveredEvent } from '../events/order-delivered.event';


@Injectable()
export class OrderListener {
 // Wildcard: catches both order.created and order.delivered
 @OnEvent(EVENTS.ORDER_ALL)
 handleOrderEvent(event: OrderCreatedEvent | OrderDeliveredEvent) {
   // Simulate delayed processing — listener logs after 10 seconds
   setTimeout(() => {
     console.log(
       `[OrderListener] Received an order event (after 10s delay): ${JSON.stringify(event)}`,
     );


     if (event instanceof OrderCreatedEvent) {
       console.log(
         `[Event: ${EVENTS.ORDER_CREATED}] Order #${event.orderId} created by User #${event.userId} — Total: $${event.total}`,
       );
     } else if (event instanceof OrderDeliveredEvent) {
       console.log(
         `[Event: ${EVENTS.ORDER_DELIVERED}] Order #${event.orderId} delivered at ${event.deliveredAt.toISOString()}`,
       );
     }
   }, 10_000); // 10 seconds
 }
}
Enter fullscreen mode Exit fullscreen mode

This listener reacts to:

  • order.created
  • order.delivered

And any event starts with order.

  • Emit Order Events
 async createOrder(orderId: number, userId: number, total: number) {
   console.log(`--- Mocking Order Creation: Order #${orderId} ---`);
   const event = new OrderCreatedEvent(orderId, userId, total);
   this.eventEmitter.emit(EVENTS.ORDER_CREATED, event);
 }


 async deliverOrder(orderId: number) {
   console.log(`--- Mocking Order Delivery: Order #${orderId} ---`);
   const event = new OrderDeliveredEvent(orderId, new Date());
   this.eventEmitter.emit(EVENTS.ORDER_DELIVERED, event);
 }


Enter fullscreen mode Exit fullscreen mode
  • Call in main.ts
await appService.createOrder(101, 1, 99.99);

await appService.deliverOrder(101);
Enter fullscreen mode Exit fullscreen mode
  • Final log should look like

Conclusion

In this article, we explored Event Emitters in NestJS in depth.
We covered:

  • Why event emitters improve responsiveness
  • How loose coupling works
  • Practical real-world use cases
  • Basic setup in NestJS
  • Advanced configuration options
  • Exact event listeners
  • Wildcard event listeners

Event emitters are a powerful tool when used correctly. They help keep systems modular, scalable, and responsive. However, they should be used intentionally, not everywhere.

Use them where independence between components creates real value.

Contact

If you have any questions, feel free to reach out:

Final code repository:

https://github.com/dedawit/nest-event-emitters.git

References

Top comments (0)