Node.js Tutorial: Creating a Promise Helper Function for your Mongoose App
Below is a quick tutorial on how to DRY your code by creating a Promise-based helper function in your Node.js/Mongoose app. The solution utilizes a JavaScript Promise object, which was first introduced to the language in ES6.
As a reminder, a Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation. Promises offer more flexibility to developers than callbacks and help you write cleaner, easy-to-read code.
Identify a contoller function that contains duplicated code. In our example, we have an Express Route handler that contains two, nearly identical Express render functions, each within their own conditional blocks. If the developer wanted to add local view variables to the template, they would have to add them to 2 places. If they needed renaming, again they would have to be renamed in both locations.
The reason this was done was because the
cart_items
andcart_total
local view variables come from different sources depending on whether a user object exists on the request object.We have highlighted the duplicate code below:
exports.getBlog = (req, res, next) => { if (req.user) { req.user .populate('cart.items.product') .execPopulate() .then(user => { res.render('newDesign/blog', { cart_items: user.cart.items, cart_total: sumPropertyValue(user.cart.items, 'quantity'), pageTitle: 'Blog', path: '/blog' }); }) .catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); } else { const cart_items = req.session.cart_items || []; res.render('newDesign/blog', { cart_items, cart_total: cart_items.length ? sumPropertyValue(cart_items, 'quantity') : 0, pageTitle: 'Blog', path: '/blog' }).catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); } };
A solution to this duplicate code problem would be to move the code that generates the
cart_items
andcart_total
values into its own helper function. Since these rely on asynchronous Mongoose calls, we will need place this code within our own Promise helper function, which we can then call from our getBlog route handler.- Create helper function with an empty JavaScript promise.
exports.getShoppingCartData = req => { return new Promise((resolve, reject) => { resolve(); reject(); }); }
- Move the conditional statements and their blocks from the route handler to our new Promise helper function:
exports.getBlog = (req, res, next) => { if (req.user) { req.user .populate('cart.items.product') .execPopulate() .then(user => { res.render('newDesign/blog', { cart_items: user.cart.items, cart_total: sumPropertyValue(user.cart.items, 'quantity'), pageTitle: 'Blog', path: '/blog' }); }) .catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); } else { const cart_items = req.session.cart_items || []; res.render('newDesign/blog', { cart_items, cart_total: cart_items.length ? sumPropertyValue(cart_items, 'quantity') : 0, pageTitle: 'Blog', path: '/blog' }).catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); } };
exports.getShoppingCartData = req => { return new Promise((resolve, reject) => { if (req.user) { resolve(); reject(); } else { resolve(); reject(); } }); }
Notice that we duplicated the resolve and reject calls to each of the possible code paths.
- Next, pull out and move to the helper the two separate methods of generating the
cart_items
array.exports.getBlog = (req, res, next) => { req.user .populate('cart.items.product') .execPopulate() .then(user => { res.render('newDesign/blog', { cart_items: user.cart.items, cart_total: sumPropertyValue(user.cart.items, 'quantity'), pageTitle: 'Blog', path: '/blog' }); }) .catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); const cart_items = req.session.cart_items || []; res.render('newDesign/blog', { cart_items, cart_total: cart_items.length ? sumPropertyValue(cart_items, 'quantity') : 0, pageTitle: 'Blog', path: '/blog' }).catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); };
exports.getShoppingCartData = req => { return new Promise((resolve, reject) => { if (req.user) { req.user .populate('cart.items.product') .execPopulate() .then(user => { const cart_items = user.cart.items; }) .catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); resolve(); reject(); } else { const cart_items = req.session.cart_items || []; resolve(); reject(); } }); }
- Next, move the
resolve()
andreject()
calls to their appropriate locations and populate with the fitted data.exports.getShoppingCartData = req => { return new Promise((resolve, reject) => { if (req.user) { req.user .populate('cart.items.product') .execPopulate() .then(user => { const cart_items = user.cart.items; resolve({ cart_items, cart_total: sumPropertyValue(cart_items, 'quantity') }); }) .catch(err => { const error = new Error(err); error.httpStatusCode = 500; reject(error); }); } else { const cart_items = req.session.cart_items || []; resolve({ cart_items, cart_total: cart_items.length ? sumPropertyValue(cart_items, 'quantity') : 0 }); } }); }
Notice that because the else block does not contain any error prone code, we have removed the
reject()
call altogether. Our Promise Helper Function is now complete and ready to be implemented within our Express route handler.
But first, we must cleanup our duplicate render calls.
exports.getBlog = (req, res, next) => { res.render('newDesign/blog', { cart_items: , cart_total: , pageTitle: 'Blog', path: '/blog' }); res.render('newDesign/blog', { cart_items, cart_total: , pageTitle: 'Blog', path: '/blog' }).catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); };
- Lastly, add a call to our Promisfied Helper function, populate its
then
method with the Express render call, and assign its fulfillment value (e.g.user_cart
) to the affected fields.
exports.getBlog = (req, res, next) => { this.getShoppingCartData(req) .then(user_cart => { res.render('newDesign/blog', { cart_items: user_cart.cart_items, cart_total: user_cart.cart_total, pageTitle: 'Blog', path: '/blog' }).catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); }) .catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); };
If the helper function is in another JavaScript module file, replace this
keyword with the variable name used in the import statement.
Notice that we have 2 catch calls. The first one catches errors with the render function, while the 2nd one catches errors with our new helper function.
Completed DRY code
exports.getShoppingCartData = req => { return new Promise((resolve, reject) => { if (req.user) { req.user .populate('cart.items.product') .execPopulate() .then(user => { const cart_items = user.cart.items; resolve({ cart_items, cart_total: sumPropertyValue(cart_items, 'quantity') }); }) .catch(err => { const error = new Error(err); error.httpStatusCode = 500; reject(error); }); } else { const cart_items = req.session.cart_items || []; resolve({ cart_items, cart_total: cart_items.length ? sumPropertyValue(cart_items, 'quantity') : 0 }); } }); } exports.getBlog = (req, res, next) => { this.getShoppingCartData(req) .then(user_cart => { res.render('newDesign/blog', { cart_items: user_cart.cart_items, cart_total: user_cart.cart_total, pageTitle: 'Blog', path: '/blog' }).catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); }) .catch(err => { const error = new Error(err); error.httpStatusCode = 500; return next(error); }); };
We have DRYed our code by creating and implementing a promise-based helper function. The resulting route handler is cleaner and easier to understand. Additionally, we can now re-use our getShoppingCartData
helper function in any of our other Express pages that require the cart_items
data.