DEV Community

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

Adding A Pub/Sub layer To Your Express Backend

ragrag profile image Raggi Updated on ・3 min read

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.

Discussion (1)

pic
Editor guide
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