Search code examples
javascriptnode.jsexpresses6-promisees6-class

Best way to export Express route methods for promise chains?


I have an API route that is being refactored to use ES6 promises to avoid callback hell.

After successfully converting to a promise chain, I wanted to export my .then() functions to a separate file for cleanliness and clarity.

The route file: enter image description here

The functions file: enter image description here

This works fine. However, what I'd like to do is move the functions declared in the Class constructor() function into independent methods, which can reference the values instantiated by the constructor. That way it all reads nicer.

But, when I do, I run into scoping problems - this is not defined, etc. What is the correct way to do this? Is an ES6 appropriate to use here, or should I use some other structure?

RAW CODE:

route...

.post((req, res) => {

  let SubmitRouteFunctions = require('./functions/submitFunctions.js');
  let fn = new SubmitRouteFunctions(req, res);

  // *******************************************
  // ***** THIS IS WHERE THE MAGIC HAPPENS *****
  // *******************************************
  Promise.all([fn.redundancyCheck, fn.getLocationInfo])
         .then(fn.resetRedundantID)
         .then(fn.constructSurveyResult)
         .then(fn.storeResultInDB)
         .then(fn.redirectToUniqueURL)
         .catch((err) => {
           console.log(err);
           res.send("ERROR SUBMITTING YOUR RESULT: ", err);
         });
  })

exported functions...

module.exports = class SubmitRouteFunctions {

   constructor (req, res) {
this.res = res;
this.initialData = {
  answers    : req.body.responses,
  coreFit    : req.body.coreFit,
  secondFit  : req.body.secondFit,
  modules    : req.body.modules,
};

this.newId = shortid.generate();
this.visitor = ua('UA-83723251-1', this.newId, {strictCidFormat: false}).debug();
this.clientIp = requestIp.getClientIp(req);

this.redundancyCheck = mongoose.model('Result').findOne({quizId: this.newId});
this.getLocationInfo = request.get('http://freegeoip.net/json/' + this.clientIp).catch((err) => err);

this.resetRedundantID = ([mongooseResult, clientLocationPromise]) => {

    console.log(mongooseResult);
    if (mongooseResult != null) {
      console.log('REDUNDANT ID FOUND - GENERATING NEW ONE')
      this.newId = shortid.generate();
      this.visitor = ua('UA-83723251-1', this.newId, {strictCidFormat: false});
      console.log('NEW ID: ', this.newId);
    };
    return clientLocationPromise.data;
  }

this.constructSurveyResult = (clientLocation) => {
    let additionalData = {quizId: this.newId, location: clientLocation};
    return Object.assign({}, this.initialData, additionalData);
  }

this.storeResultInDB = (newResult) => mongoose.model('Result').create(newResult).then((result) => result).catch((err) => err);

this.redirectToUniqueURL = (mongooseResult) => {
  let parsedId = '?' + queryString.stringify({id: mongooseResult.quizId});
  let customUrl = 'http://explore-your-fit.herokuapp.com/results' + parsedId;
  this.res.send('/results' + parsedId);
}
  }
}

Solution

  • ALTERNATIVE #1:

    Rather than using ES6 classes, an alternate way to perform the same behavior that cleans up the code just a little bit is to export an anonymous function as described by Nick Panov here: In Node.js, how do I "include" functions from my other files?

    FUNCTIONS FILE:

    module.exports = function (req, res) {
    
        this.initialData = {
          answers    : req.body.responses,
          coreFit    : req.body.coreFit,
          secondFit  : req.body.secondFit,
          modules    : req.body.modules,
        };
    
        this.newId = shortid.generate();
        this.visitor = ua('UA-83723251-1', this.newId, {strictCidFormat: false}).debug();
        this.clientIp = requestIp.getClientIp(req);
    
        this.redundancyCheck = mongoose.model('Result').findOne({quizId: this.newId});
        this.getLocationInfo = request.get('http://freegeoip.net/json/' + this.clientIp).catch((err) => err);
    
        this.resetRedundantID = ([mongooseResult, clientLocationPromise]) => {
            if (mongooseResult != null) {
              console.log('REDUNDANT ID FOUND - GENERATING NEW ONE')
              this.newId = shortid.generate();
              this.visitor = ua('UA-83723251-1', this.newId, {strictCidFormat: false});
              console.log('NEW ID: ', this.newId);
            };
            return clientLocationPromise.data;
          }
    
        this.constructSurveyResult = (clientLocation) => {
            let additionalData = {quizId: this.newId, location: clientLocation};
            return Object.assign({}, this.initialData, additionalData);
          }
    
        this.storeResultInDB = (newResult) => mongoose.model('Result').create(newResult).then((result) => result).catch((err) => err);
    
        this.redirectToUniqueURL = (mongooseResult) => {
          let parsedId = '?' + queryString.stringify({id: mongooseResult.quizId});
          let customUrl = 'http://explore-your-fit.herokuapp.com/results' + parsedId;
          res.send('/results' + parsedId);
        }
    }
    

    Although this does not avoid having to tag each method with this.someFn()..., as I originally wanted, it does take an extra step in the routing file - doing things this way prevents me from having to assign a specific namespace to the methods.

    ROUTES FILE

    .post((req, res) => {
              require('./functions/submitFunctions_2.js')(req, res);
    
              Promise.all([redundancyCheck, getLocationInfo])
                     .then(resetRedundantID)
                     .then(constructSurveyResult)
                     .then(storeResultInDB)
                     .then(redirectToUniqueURL)
                     .catch((err) => {
                       console.log(err);
                       res.send("ERROR SUBMITTING YOUR RESULT: ", err);
                     });
          })
    

    The functions are reset to reflect each new req and res objects as POST requests hit the route, and the this keyword is apparently bound to the POST route callback in each of the imported methods.

    IMPORTANT NOTE: You cannot export an arrow function using this method. The exported function must be a traditional, anonymous function. Here's why, per Udo G's comment on the same thread:

    It should be worth to note that this works because this in a function is the global scope when the function is called directly (not bound in any way).

    ALTERNATIVE #2:

    Another option, courtesy of Bergi from: How to use arrow functions (public class fields) as class methods?

    What I am looking for, really, is an experimental feature....

    There is an proposal which might allow you to omit the constructor() and directly put the assignment in the class scope with the same functionality, but I wouldn't recommend to use that as it's highly experimental.

    However, there is still a way to separate the methods:

    Alternatively, you can always use .bind, which allows you to declare the method on the prototype and then bind it to the instance in the constructor. This approach has greater flexibility as it allows modifying the method from the outside of your class.

    Based on Bergi's example:

    module.exports = class SomeClass {
    
      constructor() {
        this.someMethod= this.someMethod.bind(this);
        this.someOtherMethod= this.someOtherMethod.bind(this);
        …
      }
    
      someMethod(val) {
        // Do something with val
      }
    
      someOtherMethod(val2) {
        // Do something with val2
      }
    }
    

    Obviously, this is more in-line with what I was originally looking for, as it enhances the overall readability of the exported code. BUT doing so will require that you assign a namespace to the new class in your routes file like I did originally:

    let SubmitRouteFunctions = require('./functions/submitFunctions.js');
    let fn = new SubmitRouteFunctions(req, res);
    
    Promise.all([fn.redundancyCheck, fn.getLocationInfo])
           .then(...)
    

    PROPOSED / EXPERIMENTAL FEATURE:

    This is not really my wheelhouse, but per Bergi, there is currently a Stage-2 proposal (https://github.com/tc39/proposal-class-public-fields) that is attempting to get "class instance fields" added to the next ES spec.

    "Class instance fields" describe properties intended to exist on instances of a class (and may optionally include initializer expressions for said properties)

    As I understand it, this would solve the issue described here entirely, by allowing methods attached to class objects to reference each instantiation of itself. Therefore, this issues would disappear and methods could optionally be bound automatically.

    My (limited) understanding is that the arrow function would be used to accomplish this, like so:

      class SomeClass {
          constructor() {...}
          someMethod (val) => {
            // Do something with val
            // Where 'this' is bound to the current instance of SomeClass
          }
        }
    

    Apparently this can be done now using a Babel compiler, but is obviously experimental and risky. Plus, in this case we're trying to do this in Node / Express which makes that almost a moot point :)