DEV Community

Cover image for Adding A Pub/Sub layer To Your Express Backend
Raggi
Raggi

Posted on • Updated on

Adding A Pub/Sub layer To Your Express Backend

Adding a Pub/Sub layer to your express backend can add an event-driven capability that makes handing certain operations more intuitive as well as provide better code separation.

Sometimes we might want to perform some actions or call third party services as a result of an event occurring in our app. For example sending a welcome email, a welcome sms or analytics data when a new user is registered which is very common in most apps these days.

Lets take the aforementioned example where we send email, sms and analytic data when a user registers. Traditionally this can be done by using imperative function calls as shown in the example below.

//auth.service.ts

import EmailService from './services/mail.service';
import SMSService from './services/sms.service';
import AnalyticsService from './services/analytics.service';
//...other imports

class AuthService {
  public async signup(userData): Promise<User> {
    const findUser: User = await User.findOne({ where: { email: userData.email } });
    if (findUser) throw new Error(`Email ${userData.email} already exists`);

    const hashedPassword = await bcrypt.hash(userData.password, 10);
    const createdUser: User = await User.save({ ...userData, password: hashedPassword });

    //Some actions
    AnalyticsService.addUserRecord({email:createdUser.email, number:createdUser.number});
    EmailService.sendWelcomeEmail(createdUser.email);
    //...Other user sign up actions
    SMSService.sendWelcomeSMS(createdUser.number);

    return createdUser;
  }
}
Enter fullscreen mode Exit fullscreen mode

You can already see how this code will look like as we keep adding more actions, each action will add another imperative function call to a dependent service and the function will keep growing in size. You can also see that besides being hard to maintain, this approach violates the Single Responsibility Principle as well as has the potential for repetition across different events not only user registration.

Pub/Sub Layer

Adding a Pub/Sub layer can solve this problem by emitting an event (user registered with this email) and letting separate listeners handle the work.

We will utilize Node.js's Event Emitter to do that.

First we will create a shared Event Emitter as well as specify the set of events we need.

//eventEmitter.ts
import { EventEmitter } from 'events';

const Events = {
  USER_REGISTRATION = 'user-registered',
}
const eventEmitter = new EventEmitter();

export { eventEmitter, Events };
Enter fullscreen mode Exit fullscreen mode

Note: Due to Node.jS Caching, this will always return the same instance of eventEmitter (Singleton)

Now we can modify our code to emit a "user registration event"

//auth.service.ts

import { eventEmitter, Events } from '../common/utils/eventEmitter';
//...other imports

class AuthService {
  public async signup(userData): Promise<User> {
    const findUser: User = await User.findOne({ where: { email: userData.email } });
    if (findUser) throw new Error(`Email ${userData.email} already exists`);

    const hashedPassword = await bcrypt.hash(userData.password, 10);
    const createdUser: User = await User.save({ ...userData, password: hashedPassword });

    //Emit User Registration Event
    eventEmitter.emit(Events.USER_REGISTRATION,{ email: userData.email, number: userData.number });

    return createdUser;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now Separate Services can listen on events and do their Job, For example the EmailService

//email.service.ts

import MailGunClient from '../common/clients/mailGun.client';
import EmailClient from '../common/interfaces/emailClient.interface';
import { eventEmitter, Events } from '../common/utils/eventEmitter';

class EmailService {
  constructor(private emailClient: EmailClient = new MailGunClient()) {
    this.initializeEventListeners();
  }

  private initializeEventListeners(): void {
    eventEmitter.on(Events.USER_REGISTRATION, ({ email }) => {
      this.emailClient.sendWelcomeEmail(email);
    });
  }
}

export default EmailService;
Enter fullscreen mode Exit fullscreen mode

Now all that is left is to create an instance of your event listening services when bootstrapping your express app to initialize their listeners, something like calling this function when initializing your app

  private initializeServices() {
    new AnalyticsService();
    new EmailService();
    new SMSService();
  }
Enter fullscreen mode Exit fullscreen mode

You can already see how adding more actions won't add any extra lines of code in the user registration function which provides code separation and embraces the event driven nature of Node.js.

Top comments (5)

Collapse
 
captdaylight profile image
captDaylight • Edited

Great article Raggi, clear and concise. Thanks for writing.

How would you define what parameters the event is expecting with typescript?

Collapse
 
vanenshi profile image
Amir Hossein Shekari
Collapse
 
ciochetta profile image
Luis Felipe Ciochetta

this is nice, I've always liked event-driven architectures for game development but never tried to use in node

Collapse
 
hmarcelodnae profile image
Marcelo Del Negro

Hey, what if the event handler failed? won't you have lost the oportunity to handle this? I dont believe it can be considered a reliable way of managing side effects... what do you think?

Collapse
 
ragrag profile image
Raggi

yeah that's a very valid point, i agree with you about the inability to catch downstream errors.
probably it can be more suitable to use this for safer side effects