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
2 Install Event Emitter Package
npm i --save @nestjs/event-emitter
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 {}
This is the minimal configuration
Advanced Configuration
EventEmitterModule.forRoot({
wildcard: false,
delimiter: '.',
newListener: false,
removeListener: false,
maxListeners: 10,
verboseMemoryLeak: false,
ignoreErrors: false,
});
Configuration Explained
- wildcard
Allows pattern-based event names. If false, only exact names match.
@OnEvent('user.created')
Only listens to:
user.created
If true:
@OnEvent('user.*')
Listens to:
user.created
user.deleted
user.updated
- delimiter
Used with namespaces. Default commonly uses .
Examples:
user.created
order.shipped
payment.completed
You may also use:
user:created
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', () => {});
Output:
Added listener for: user.created
Added listener for: order.created
- removeListener
Emits removeListener when a listener is removed.
emitter.on('removeListener', (event) => {
console.log('Removed listener from:', event);
});
emitter.off('user.created', handler);
Output:
Removed listener from: user.created
Usually rarely needed.
- maxListeners
Warns when too many listeners are attached to one event.
maxListeners: 5
This is not a hard limit. It warns about possible memory leaks.
- verboseMemoryLeak
Controls warning detail.
If false
Possible memory leak detected
If true
Possible memory leak detected. Event name: user.created
- ignoreErrors
Controls behavior of special error event.
emit('error', new Error('Payment gateway down'));
If:
ignoreErrors: false
Unhandled error events throw exceptions. Good during development.
If:
ignoreErrors: true
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,
) {}
}
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',
};
- 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}`,
);
}
}
- 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);
}
- 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();
- Register Listener
providers: [AppService, UserCreatedListener],
- 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: '.',
}),
- Register Provider
providers: [AppService, UserCreatedListener, OrderListener],
- 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,
) {}
}
- Add Constants
export const EVENTS = {
USER_CREATED: 'user.created',
ORDER_CREATED: 'order.created',
ORDER_DELIVERED: 'order.delivered',
ORDER_ALL: 'order.*',
};
- 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
}
}
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);
}
- Call in main.ts
await appService.createOrder(101, 1, 99.99);
await appService.deliverOrder(101);
- 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:
- LinkedIn: https://linkedin.com/in/dawit-girma-7b8867228/
- Email: realdavis7779@gmail.com
- GitHub: https://github.com/dedawit
Final code repository:
https://github.com/dedawit/nest-event-emitters.git


Top comments (0)