Adding A Pub/Sub layer To Your Express Backend
Tags: backend, express, node, api
Published: 2020-12-10 15:36:48

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.

```js
//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;
  }
}
```
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](https://en.wikipedia.org/wiki/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](https://nodejs.org/api/events.html) to do that. 

First we will create a **shared** Event Emitter as well as specify the set of events we need.
```js
//eventEmitter.ts
import { EventEmitter } from 'events';

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

export { eventEmitter, Events };
```
**_<sub>Note: Due to [Node.jS Caching](https://nodejs.org/api/modules.html#modules_module_caching_caveats), this will always return the same instance of eventEmitter (Singleton)</sub>_**

Now we can modify our code to emit a "user registration event"
```js
//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;
  }
}
```

Now Separate Services can listen on events and do their Job, For example the **EmailService** 
```js
//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;
``` 
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
```js
  private initializeServices() {
    new AnalyticsService();
    new EmailService();
    new SMSService();
  }
```
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.