Search code examples
javascriptnode.jsexpresseventemitter

Creating a Singleton EventEmittor in Nodejs & Express Project


I'm trying to create a Publisher-Subscriber pattern with Nodejs eventEmitters for my nodejs/express API project. My use-case is fairly simple:

  1. I want to emit events from various places in my project. For example, emit an event when a new user is created.
  2. Then I want a single place, where I listen to all the events and decide what actions to take.

This will require a singleton instance of eventEmitter because 'emit' and 'on' calls should happen on the same instance of 'event' object.

To achieve it, this is how my index.js looks:

const express = require('express')
const app = express()

const bodyParser = require('body-parser')
const routes = require('./api/routes')
const EventEmitter = require("events");
const eventEmitter = new EventEmitter();

const eventListeners = require('../src/subscribers/events');
//const eventSubs = new eventListeners();

app.use(eventListeners.eventSubscribers());

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.set('eventEmitter', eventEmitter);

app.get('/', (req, res) => res.send('App is working'))

app.use('/api', routes)

app.listen(3000, () => console.log('Example app listening on port 3000!'))

module.exports = {
  app
}

This is how my middleware/controller function looks(where I'm emitting event):

const createUser = async (req, res, next) => {
  var user = req.body
  try {
    console.log('in controller');

    await newUser(user)

    console.log(req.app.get('eventEmitter'));

    req.app.get('eventEmitter').emit('USER_CREATED', {email: user.email, name: user.name})

    res.status(200).send({success: true});
    next()

  } catch(e) {
    console.log(e.message)
    res.sendStatus(500) && next(e)
  }
}

And, this is how my event subscriber file looks:

const eventSubscribers = (req) => {
    const eventEmitter = req.app.get('eventEmitter');

    eventEmitter.on('USER_CREATED', ({ email, name }) => {
        console.log('event fired and captured')
    })
}

Using this, the event is getting emitted, but the event subscriber is not getting trigerred. I think there is something wrong with the way I'm accessing req/app objects in my subscriber. Also, I'm getting

TypeError: eventListeners.eventSubscribers is not a function

error in my index.js file. Questions I have:

  1. Is this the right way to create a publisher/subscriber pattern?
  2. How do I access req/app object instances in my event subscriber?
  3. What's the best way to make sure event subscriber file is loaded when the app loads?

Solution

  • Assuming everything else is correctly setup by you,
    I think the cause of the error that you mentioned

    TypeError: eventListeners.eventSubscribers is not a function
    

    is that no function is passed to the middleware i.e as a middleware, shouldn't you just pass the eventListeners.eventSubscribers like this

    app.use(eventListeners.eventSubscribers);
    

    instead of like this

    app.use(eventListeners.eventSubscribers());
    

    If we compare both expressions above, express is not able to pass the request object in the second case as you are calling it but this function call is returning nothing to the express i.e kind of void, but in first case since you passed the function object reference to express, it then calls it as middleware(as a callback later and passes the request object in it while calling it).

    Alternatively I think you don't need to use middleware for using the EventEmitter, you can just keep a single global custom instance consisting of EventEmitter as it's property and all the different event you want to listen, and then just use them wherever you require them.You can use the EventEmitter as a property of this custom to emit event in whatever module you want to by importing this custom module there.

    This is the working example below

    index.js file/entry file

    const express = require('express')
    const app = express()
    const bodyParser = require('body-parser')
    const routes = require("./userRoute");
    //const eventSubs = new eventListeners();
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));
    
    app.get('/', (req, res) => res.send('App is working'))
    
    app.use('/api', routes);
    
    app.listen(3000, () => console.log('Example app listening on port 3000!'))
    

    This is our global singleton custom instance using EventEmitter as a property.

    const { EventEmitter } = require("events");
    
    class CustomEventEmitter {
    
        eventEmitter;
    
        constructor() {
            this.eventEmitter = new EventEmitter();
            this.initialize();
        }
    
        getEventEmitter() {
            return this.eventEmitter;
        }
    
        initialize() {
            this.eventEmitter.on('USER_CREATED', ({ email, name }) => {
                console.log('event fired and captured with data', email, name);
            });
        }
    }
    
    module.exports = new CustomEventEmitter();
    

    and here we use our global CustomEventEmitter module in our request routes i.e userRoute.js

    const router = require("express").Router();
    const customEventEmitter = require("./CustomEventEmitter");
    
    const createUser = async (req, res, next) => {
        var user = req.body
        try {
            console.log('in controller');
    
            // await newUser(user)
    
            customEventEmitter.getEventEmitter().emit('USER_CREATED', { email: user.email, name: user.name })
    
            res.status(200).send({ success: true });
            next()
    
        } catch (e) {
            console.log(e.message)
            res.sendStatus(500) && next(e)
        }
    }
    
    router.post("/createUser", createUser)
    
    module.exports = router;