The async/await model in REST endpoints using Express and Node.js
Functions that use Promises can offer us synchronisation, but also result in a nested, hard to maintain and read code. The newest version of Node.js introduces the async/await model. In this post, I will demonstrate how to implement it in a REST endpoint as part of our Express server, extending my blog post about Sharing MySQL Pool connections between files with Node.js.
Installing the modules
The required packages for this example are express, jsonschema, body-parser, promise-mysql. I analyze their usage in my previous blog posts. To install them run the following:
npm install express jsonschema body-parser promise-mysql
Creating the MySQL pool and sharing connection as middleware
We will modify our previously exported module, to be able to share the requested connection from the MySQL pool. To do that, we need to pass the connection to an object of higher scope level. Since our middleware receives as arguments the request, response and next objects, we will use the request object to pass the connection to the next function.
var mysql = require('promise-mysql');
var connectionPool = mysql.createPool({
host : "127.0.0.1",
user : '**********',
password : '**********',
database : '**********',
port : '3306'
});
module.exports={
getConnection: function(req, res, next){
return new Promise(function(resolve,reject){
connectionPool.getConnection().then(function(connection){
req.connection = connection;
next();
}).catch(function(error){
res.status(500).end("Error getting connection: " + error);
});
});
}
}
Error handling in Promises
When a Promise fails it throws an error that can be caught and managed by our try/catch blocks. But, in the future versions of Node.js, Unhandled promise rejections will terminate our application. Managing these is fairly simple:
process.on('uncaughtException', error => console.error('Uncaught exception: ', error));
process.on('unhandledRejection', error => console.error('Unhandled rejection: ', error));
Using async/await on a REST endpoint
In order to use the async/await model in a REST endpoint, we must consider that an "await for a promise" command requires an async function to call them. A fairly simple way of doing it is having the async keyword on our last RESTful endpoint's function. Considering that we use our imported middleware for JSON input Validation and Requesting MySQL connection, we have:
app.post('/add/product', validate.JsonValidation, mysqlConnection.getConnection, async function(req,res){
var connection = req.connection;
try {
item.seller = await findOrCreateUser(user,connection);
var user_id = await addItem(item,connection);
var items = await selectUserItems(user_id, connection);
connection.connection.release();
res.status(200).json(result);
} catch(error) {
res.status(500).end(error);
}
});
The complete code of our example is shown below:
var express = require('express');
var bodyParser = require('body-parser')
var app = express();
var validate = require('./validate')
var mysqlConnection = require('./connectionShare');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
const addItem = function(item, connection){
console.log("Adding Item");
return new Promise(function(resolve, reject){
connection.query("INSERT INTO product SET ?", item)
.then(function(result){
resolve(item.seller);
}).catch(function(error){
reject(error);
});
})
}
const findOrCreateUser = function(user,connection){
console.log("Finding User");
return new Promise(function(resolve,reject){
connection.query("SELECT * FROM user WHERE email=" + connection.escape(user.email))
.then(function(results){
if(results.length == 1){
resolve(results[0].id)
} else {
connection.query("INSERT INTO user SET ?", user)
.then(function(results){
resolve(results.insertId);
});
}
}).catch(function(error){
reject(error);
})
})
}
const selectUserItems = function(userID,connection){
console.log("Selecting Items " + userID);
return new Promise(function(resolve,reject){
connection.query("SELECT * FROM product WHERE seller = " + connection.escape(userID))
.then(function(results){
resolve(results);
}).catch(function(error){
reject(error);return;
});
})
}
app.post('/add/product', validate.JsonValidation, mysqlConnection.getConnection, async function(req,res){
var connection = req.connection;
var item = {
name: req.body.name,
price: req.body.price,
width: req.body.width,
height: req.body.height,
added: req.body.added,
image: req.body.image
};
var user = {
username: req.body.seller.username,
email: req.body.seller.email,
votes: req.body.seller.votes
};
try {
item.seller = await findOrCreateUser(user,connection);
var user_id = await addItem(item,connection);
var items = await selectUserItems(user_id, connection);
connection.connection.release();
res.status(200).json(result);
} catch(error) {
res.status(500).end(error);
}
});
process.on('uncaughtException', error => console.error('Uncaught exception: ', error));
process.on('unhandledRejection', error => {console.error('Unhandled rejection: ', error));
app.listen(8000, function () {
console.log('App listening on port 8000')
});