How you can use Events and Event-Emitters with NestJs.
Events are occurrences, actions that have happened. These actions can trigger other actions. When coming to the field of software engineering, they are actions that are performed by different components of an application. Say for example, a food ordering app. This app is made up of the food service — the component that shows the user different types of food that are ready to be bought, order service — the component that helps the user order for the desired meal, and then the payment service — the component that helps the user pay for the food with whatever payment gateway that is available in the app.
This may not be the correct system design of a food ordering app in production, but you get the idea. So after the user makes payment for the meal, an email notification containing the receipt of the payment is sent to the customer. How did this happen? This is probably from another service, let’s say the receipt service — a component that sends a PDF format of a receipt after every payment made.
Now it is possible to write all these functions into one service with the relevant data, right? But it is good development practice to break your application into little bits of modules for easy readability. For illustration, we are going to build a demo food ordering API to explain the technical use of events and event-emitters.
Requirements: you gotta have the nestjs framework and probably Docker/docker-compose(not required) installed in your computer.
- Starting the Nest application, run in your favourite IDE terminal (VSCode in my case):
nest new food_demo
- You should have a folder similar to this:
- Now the next few steps are optional, but for us to be on the same page, I’d advise you actually install Docker and follow me.
I prefer containerizing my applications at the beginning of development. So to put your application in a Docker container, create a file called Dockerfile (yes you read that correctly, no extensions) in the root directory and fill it with the following:
# Building the Docker image from the base image Node:18-apline
FROM node:18-alpine# creating the working directory of the Docker image
WORKDIR /usr/src/app# copy package.json and package-lock.json to the image working directory
COPY package*.json ./# Install app dependencies
RUN npm install# COPY the Nest application into the image
COPY . .# build the application
RUN npm run build
- Create a .env file in the root folder that will contain your environment variables needed by docker-compose.yml. The content of the file should be similar to:
DB_USERNAME=user
DB_PASSWORD=user
DB_NAME=food_repo
DB_HOST=food_repo_postgres # the container name of the postgres service
DB_PORT=5432
- To manage your Docker application, you have to create a docker-compose.yml file in the root directory as well. Its content:
version: '3.7'services:# service for web application
web:
container_name: food_web
build:
context: .
ports:
- 9229:9229
- 3000:3000
command: npm run start:devvolumes:
- .:/usr/src/app
- /usr/src/app/node_modulesenv_file:
- .env
depends_on:
- postgres
# service for postgres database
postgres:
container_name: food_repo_postgres
image: postgres:12-alpine
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
POSTGRES_HOST: ${DB_HOST}
POSTGRES_PORT: ${DB_PORT}ports:
- 5432:5432
volumes:
- pgdata:/var/lib/postgresql/datavolumes:
pgdata:
The web service here is your containerized Nest application. The postgres service makes it possible to use a PostgreSQL RDBMS without actually installing one into your system, with the help of a PostgreSQL Docker image.
- Now start up the containers:
docker-compose up
This commad should build your Docker images and run them as well. You should see something similar to the image below:
Yayy. You just ran your application in a Docker container… now shut it down. Yes, do it:
docker-compose down
Now you will have to install the following packages:
npm install @nestjs/typeorm typeorm pg @nestjs/event-emitter class-validator class-transformer
@nestjs/typeorm here is the package that helps you communicate to the TypeORM package from NestJs.
typeorm is TypeORM itself.
pg is the database client for PostgreSQL.
@nestjs/event-emitter is the EventEmitter2 library, built for NestJs.
class-validator is a package that will be used to validate data transfer objects in our POST requests.
class-transformer works together with the class-validator in serializing and deserializing of data.
Speaking of validating data, you will have to add the NestJs ValidationPipe in the engine of your application — main.ts file:
import { ValidationPipe } from '@nestjs/common'; //new
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';async function bootstrap() {
const app = await NestFactory.create(AppModule);
// new
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // removes unnecessary properties in POST request body
transform: true,
}),
);await app.listen(3000);
}
bootstrap();
- After installing these packages, you have to register them in your Docker application with the command
docker-compose build
- What’s next? Now you have to add database configuration to the application. This can be done in the app.module.ts file:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres', // TypeORM using the PostgreSQL database
host: process.env.DB_HOST,
port: parseInt(process.env.PORT),
database: process.env.DB_NAME, // the name of the database
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
entities: []
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
You’ve just finished setting up your application to work with the PostgreSQL Docker image. You now have to create a module that will contain the food menu data. You can do this by running the commands:
nest generate module foodMenu
nest generate service foodMenu
nest generate controller foodMenu
foodMenu here is the name of your module, service and controller. You should see new files in your directory:
If you do not know already, generating a module is just like creating a new folder for your foodMenu feature. The service command generates files for your foodMenu business logic, and the controller command will create files for routes of your business logic.
- Creating a data model of a meal for your foodMenu module should be done in an entity file. In the food-menu module, create a new file, let’s say food.entity.ts. It should contain the following:
// import decorators from typeORM
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";// decorator for creating an entity
@Entity()
export class Food {// decorator to automatically generate an id for a newly created entity
@PrimaryGeneratedColumn()
id: number@Column()
name: string@Column()
price: number
}
- Registering the entity with the food-menu.module.ts file:
import { Module } from '@nestjs/common';
import { FoodMenuService } from './food-menu.service';
import { FoodMenuController } from './food-menu.controller';import {TypeOrmModule} from '@nestjs/typeorm' // new
import {Food} from './food.entity' //new@Module({
//new
imports: [
TypeOrmModule.forFeature([Food])
],providers: [FoodMenuService],
controllers: [FoodMenuController]
})
export class FoodMenuModule {}
- The next step is to register this entity to our data source — PostgreSQL, in the app.module.ts file:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FoodMenuModule } from './food-menu/food-menu.module';
import {Food} from './food-menu/food.entity' //new@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.PORT),
database: process.env.DB_NAME, // the name of the database
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
entities: [Food], //new
synchronize: true,
}),
FoodMenuModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
- Now for simplicity’s sake, we will define the business logic that creates and gets the menu items in the food-menu.service.ts.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {Repository} from 'typeorm'
import { Food } from './food.entity';@Injectable()
export class FoodMenuService {
constructor(
@InjectRepository(Food) private foodRepo: Repository<Food>
) {}async createMenuItem (name: string, price: number) {
const food = this.foodRepo.create({ name, price})
return await this.foodRepo.save(food)
}async getMenuItems () {
return this.foodRepo.find()
}
}
Up next, you’ll have to create your data transfer object of POST request body.
- Create a new dtos folder in the food-menu folder, and create a createMenuItem.dto.ts file, which will contain:
import {IsString, IsNotEmpty, IsNumber} from 'class-validator'export class CreateMenuItemDto {@IsNotEmpty()
@IsString()
name: string@IsNotEmpty()
@IsNumber()
price: number
}
- Then for the food-menu routes, we define them in the food menu.controller.ts file:
import { Controller, Post, Body, Get } from '@nestjs/common';
import {FoodMenuService} from './food-menu.service'
import {CreateMenuItemDto} from './dtos/createMenuItem.dto'@Controller('food-menu')
export class FoodMenuController {
constructor(
private foodMenuService: FoodMenuService
) {}@Post()
async createMenuItem ( @Body() body: CreateMenuItemDto ) {
return this.foodMenuService.createMenuItem(body.name, body.price)
}@Get()
async getMenuItems () {
return this.foodMenuService.getMenuItems()
}
}
So you have created the food menu feature, but we want to notify your users of when a new meal has been added to the menu. Here’s what you do.
To the big moment:
- First you have to register the EventEmitter module in the app.module.ts file:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FoodMenuModule } from './food-menu/food-menu.module';
import {Food} from './food-menu/food.entity'
import {EventEmitterModule } from '@nestjs/event-emitter' //new@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.PORT),
database: process.env.DB_NAME, // the name of the database
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
entities: [Food],
synchronize: true,
}),
FoodMenuModule,
EventEmitterModule.forRoot() //new
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
- Create an events folder in your src directory.
- Create a new.meal.event.ts file in the folder. This will be used to define an event that will notify the user of when a new meal is added to the menu.
- The new.meal.event.ts should contain a short block of code:
export class NewMealEvent {
constructor (public name: string) {}
}
Now to create the feature that will notify the users, you can run the commands in your terminal:
nest generate module notifications
nest generate service notificatons
nest generate controller notifications //optional
- Edit the the food-menu.service.ts:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {Repository} from 'typeorm'
import { Food } from './food.entity';
import {NewMealEvent} from '../events/new.meal.event' //new
import { EventEmitter2 } from '@nestjs/event-emitter'; //new
@Injectable()
export class FoodMenuService {
constructor(
@InjectRepository(Food) private foodRepo: Repository<Food>,
private eventEmitter: EventEmitter2
) {}async createMenuItem (name: string, price: number) {
const food = this.foodRepo.create({name, price})
await this.foodRepo.save(food) this.eventEmitter.emit('new.meal',
new NewMealEvent(food.name)
) //new return food
}async getMenuItems () {
return this.foodRepo.find()
}
}
From the above code, you can see that you emitted an event called new.meal, which contains the data — name of the menu item(food.name), after it was saved in the database.
- Now the notifications’ service can always listen to this event and notify the user when a new meal has been added, with the help of the OnEvent() decorator:
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {NewMealEvent} from '../events/new.meal.event'@Injectable()
export class NotificationsService {@OnEvent('new.meal')
async notifyUser (payload: NewMealEvent) { console.log(`Hello user, ${payload.name} has been added to our menu. Enjoy.`) }
}
The OnEvent() decorator listens to a new.meal event, when there’s one, the notifyUser method is executed.
To test it out:
Start up the containers, docker-compose up
, open your favourite API client, mine is Insomnia. Make a POST request to localhost:3000/food-menu with the required data as shown below:
You should see the notifyUser method executed in your terminal:
And there you have it.
Hopefully this has been of help to someone. You can also check out the repo. Please feel free to reach out to me on LinkedIn or Twitter. See you next time.
Till then, keep coding.