An approach to a Nest notification module

Maia Viera Cañive
6 min readApr 6, 2021

It is usual we need to notify interested parts about certain action executed by other user. Notifications could be address, in many cases, using an Aspect Oriented Programming. This paradigm allows to add or remove cross-cutting business logic smoothly. Nest.js offer Interceptors to easily implement this pattern. In the following sections, we will offer a simple way to implement email notifications in any Nest Application.

It will be assumed you already have a running Nest application with a Controller A serving Endpoint A1. If not, it could be done running Nest CLI command:

nest new nest_notifier_app

If you have used CLI command and you have a freshly created Nest App, it can be assumed that your controller is AppController with an endpoint as follows:

@Get()
getHello(): string {
return this.appService.getHello();
}

Starting with notification module

Create a new module. Name it notice. Inside its folder, we should have two files: notice.module.ts and notice.service.ts. In addition, in root directory of module, we will create three folders: decorators, interceptors and types. We will be explaining the purpose of each folder and their files immediately.

Decorator

We’ll use Decorators to declare point-cuts. Cross-cutting concern, in this case email notifications, will apply after the successful execution of the method decorated. Inside folder decorators create the file notice.decorator.ts which will have the following code.

import { SetMetadata } from '@nestjs/common';
import { NotifyOptions } from '../types/options.class';
export const META_NOTICE = 'notice_metadata';
export const Notify = (options: NotifyOptions) =>
SetMetadata(META_NOTICE, options);

As you can see this module depends of class NotifyOptions. Create a file inside folder types and name it options.class.ts, then add the following code:

export class NotifyOptions {
msg: string;
emailListID: string;
absoluteRefResource?: string;
relativeRefResource?: string;
}

NotifyOptions class contains the information that will be included in the email notification sent when the decorated route handler method is called. Those are message, email list id, and links to resources (absolute link and relative link). The first two are mandatory.

Once decorator @Notify is declared, please, go to controller file in this case AppController and find endpoint getHello(). Adding @Notify decorator allows declaring a point-cut for notification logic in this endpoint.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { Notify } from '../notice/decorators/notice.decorator';
import { EMAIL_LISTS_ID } from '../user/constants/email-lists- id.enum';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@Notify({
msg: 'This message is sent by AppController → geHello() endpoint',
emailListID: EMAIL_LISTS_ID.All_Registered_Users,
})
getHello(): string {
return this.appService.getHello();
}

In previous code appears EMAIL_LISTS_ID enumerator, which has not been declared yet, but it will be declared soon. This enumerator is used to keep email list ids consistent and centralized throughout the code.

So far, we have been able to create the code needed to define where the notification logic should be waived. Then, is time to implement the notification logic.

Notice Service

Let’s open notice.service.ts. Before starting codding it is necessary to install module Mailer. Mailer module wraps the popular nodemailer library into a Nest module for sending emails.

Once the module has been installed (please follow the instructions of authors), it is possible to implement the notice service.

import { MailerService } from '@nestjs-modules/mailer';
import { Inject, Injectable } from '@nestjs/common';
import { NotifyOptions, ServiceOptions } from './types/options.class';
@Injectable()
export class NoticeService {
async notifyByEmail(notice: NotifyOptions, req: any) {
//Users list
const list: string = await
this._emailLists.getList(notice.emailListID);
if (!list) {
console.log('There is not users to notify');
return;
}
//Links
const link = notice.absoluteRefResource
? notice.absoluteRefResource : ‘’;
//Composing email text
let htmlmsg = `<strong>${notice.msg}</strong><br>
${link ? 'Resource: ' + link + '<br>' : ''}`;
let msg = `${notice.msg}\n${link ? 'Resource: ' + link + '\n'
: ''}`;
//Sending emails using Mailer
console.log(list, msg);
try {
await this._mailer.sendMail({
to: list,
subject: 'Automatic Notification from APP', // Subject line
text: `${msg}`, // plaintext body
html: `${htmlmsg}`, // HTML body content
});
} catch (exception) {
console.log(exception);
}
}
}

The service also receives a parameter named req, which is the Request object of Express, it could be used to enrich the notification email; for example, accessing the logged user for supplying information about sender, etc. In this case, req will be used for creating an absolute path using the relative path of NotifyOptions. Modify the Link section of previous code as follows:

if (notice.relativeRefResoruce)
notice.relativeRefResource = `${req.protocol}://${req.hostname}:
3000/${notice.relativeRefResource }`;
const link = notice.relativeRefResource ?
notice.relativeRefResource : notice.absoluteRefResource;

As you may have noticed in line const list: string = await this._emailLists.getList(notice.emailListID); the attribute _emailLists has not been declared yet. This line obtains the list of emails from an email list id. Let’s declare this attribute, we’ll do it using dependency injection in the constructor of the service.

constructor(private _mailer: MailerService,
@Inject('EMAIL_LISTS_PROVIDER') private _emailLists: any
) {}

Now _emailLists is defined, but it is defined as an injectable provider. This is because Notice Module does not need to host the logic for choosing the list of users to be notified, as it is very specific to the application being implemented, this logic should be provided to the module.

EMAIL_LISTS_PROVIDER must implement the interface EmailLists, which basically has only one method getList(). Therefore, create a new file in folder types and name it email-list.interface.ts, then insert the following code.

export interface EmailLists {
getList(id: string);
}

Interceptor

Now is time to declare the Interceptor responsible for calling the notification logic at every successful execution of endpoints decorated with @Notify. Go to folder interceptors and create a new file notice.interceptor.ts. Add the following code.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { META_NOTICE } from '../decorators/notice.decorator';
import { NoticeService } from '../notice.service';
import { NotifyOptions } from '../types/options.class';
@Injectable()
export class NoticeInterceptor implements NestInterceptor {
constructor( private _noticeService: NoticeService,
private _reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler):
Observable<any>
{
//Determine if request is for notification
const notice = this.mergeNotice(
this._reflector.get<NotifyOptions>(META_NOTICE,
context.getClass()),
this._reflector.get<NotifyOptions>(META_NOTICE,
context.getHandler()),
);

if (!notice) {
return next.handle();
}
//Call notification logic
const req = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => this._noticeService.notifyByEmail(notice, req)),
tap(() => console.log('Notified by Notice Module')),
);
}
mergeNotice(obj1: NotifyOptions, obj2: NotifyOptions):
NotifyOptions
{
return {...obj1,...obj2,};
}
}

Interceptor is simple. The constructor helps to inject needed providers, in this case NoticeService, and utility class Reflector for screening which endpoints are decorated for notification.

After that, is declared method intercept; it has two parts. First part determines if the request should be notified and the second part call the notification logic. Method mergeNotice() just assists creating only one NotifyOptions object if decorator has been used at controller and handler level.

Module class

Finally, to complete the Notice Module it is necessary to declare class NoticeModule. Open the file notice.module.ts and add the following code.

import { MailerModule } from '@nestjs-modules/mailer';
import { DynamicModule, Inject, Module } from '@nestjs/common';
import { NoticeService } from './notice.service';
import { OptionsNoticeModule } from './types/options.class';
@Module({})export class NoticeModule {
static forRootAsync(options: OptionsNoticeModule): DynamicModule {
return {
module: NoticeModule,
imports: [
MailerModule.forRootAsync({
useFactory: () => ({
transport: process.env[options.envTransport],
defaults: { from: options.from,},
}),
}),
...options.listsImports,
],
providers: [
{
provide: 'EMAIL_LISTS_PROVIDER',
useExisting: options.listsProvider,
},
NoticeService,
],
exports: [],
};
}
}

This module should be registered using a forRootAsync() method, it is a good option considering we need to dynamically provide configuration options. The method receives a parameter of type NoticeModuleOptions, define this class adding the following code to options.class.ts file found in folder types. The attributes of the class correspond to configurable options of Notice Module.

export class NoticeModuleOptions { 
listsImports: any[];
listsProvider: any;
envTransport: string;
from?: string;
}

Once the method is called, Mailer is registered and imported using parameters envTransport and from. Of course, NoticeModuleOptions can include all other parameters needed to dynamically and completely configure this module.

After importing Mailer, must be imported the modules needed for the provider in charge of obtaining the email list of all users that should be notified. For that is this line …options.listsImports.

Then it is time for providers. Providers of this module include the email list provider sent as a configurable option, and NoticeService. That is all.

How to use

Provider EMAIL_LISTS_PROVIDER could be a user service declared in a hypothetical user module of application. The following is an example of the hypothetical user service.

import { Injectable } from '@nestjs/common'; 
import { EmailLists } from '../notice/types/email-list.interface';
import { EMAIL_LISTS_ID } from './constants/email-lists-id.enum';
@Injectable()
export class UserService implements EmailLists {
async getList(id: string): Promise<string[]> {
switch (id) {
case EMAIL_LISTS_ID.All_Registered_Users:
return ['user1@dom1'];
}
return [];
}
}

In the same User Module could be define enumerator EMAIL_LISTS_ID.

export enum EMAIL_LISTS_ID {
All_Registered_Users = 'All Users',
}

Then, Notice Module can be registered as follows.

NoticeModule.forRootAsync({ 
listsProvider: UserService,
listsImports: [UserModule],
envTransport: EMAIL_SMTP_CONNECTION,
}),

Interceptor could be called globally in bootstrap function of main.ts file as follows.

const noticeService = app.get(NoticeService);
const reflector = app.get(Reflector);
app.useGlobalInterceptors(new NoticeInterceptor(noticeService,
reflector));

Improvements

This module can still be considerably improved. Below, there is a list of possible additions to make this module much more complete.

  • Include other ways to to notify, not just email.
  • Option of gather daily notifications and send only one email.
  • Addition of all other configurable options of Mailer, such as templates.

--

--