Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
Express router functions can contain one or more middleware functions. Middleware functions execute during the lifecycle of a request to any Express router function.
Middleware functions have access to the req and res objects.
Middleware functions also have access to the next middleware function in the application’s request-response cycle. If the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function.
Middleware functions can:
The order that middleware functions are written is important. A router will step through the middleware functions in the order that they are declared.
The first parameter of a router is always the path. This is always followed by one or more middleware functions. Middleware functions can be declared inside a router function. This is what we have being doing in our previous examples. In the logout router below, the highlighted code is an embedded middleware.
router.post(`/users/logout`, (req, res) => { res.json({}) })
A middleware function can be written as a separate function. This function can then be added as a route middleware function parameter
const logout = (req, res) => { res.json({}) } router.post(`/users/logout`, logout)
We can separate functionality by breaking a middleware function into several smaller, user-defined, middleware functions. For example, the routers that deal with reading, adding, editing and deleting one car document all require that the user is logged in. The login verification code can be moved to its own user-defined middleware function.
// Verify login code embedded inside the original middleware function // Read one record router.get(`/cars/:id`, (req, res) => { jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => { if (err) { res.json({errorMessage:`User is not logged in`}) } else { carsModel.findById(req.params.id, (error, data) => { res.json(data) }) } }) }) // Verify login code moved to a separate, user-defined, middleware function const verifyUsersJWTPassword = (req, res, next) => { jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => { if (err) { res.json({errorMessage:`User is not logged in`}) } else { req.decodedToken = decodedToken next() } }) } // Read one record router.get(`/cars/:id`, verifyUsersJWTPassword, (req, res) => { carsModel.findById(req.params.id, (error, data) => { res.json(data) }) })
Each middleware function must either return a response to the client-side or call the next() middleware function.
Replace the add, update and delete cars router code so that they all use the verifyUsersJWTPassword middleware function.
In order to increase readability and maintainability, can replace all of the embedded middleware functions with user-defined middleware functions. For example, the "Read one record" router above can be further reduced, as shown in the code below:
// Verify login code moved to a separate, user-defined, middleware function const verifyUsersJWTPassword = (req, res, next) => { jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => { if (err) { res.json({errorMessage:`User is not logged in`}) } else { req.decodedToken = decodedToken next() } }) } // reading a car document's code moved to a separate, user-defined, middleware function const getCarDocument = (req, res) => { carsModel.findById(req.params.id, (error, data) => { res.json(data) }) } // Read one record router.get(`/cars/:id`, verifyUsersJWTPassword, getCarDocument)
We use the router.use() method to add middleware that will apply to all of the routes in a given file. In the example below, the logger middleware function will be called before any other middleware function for every route in the given file.
const router = require(`express`).Router() const logger = (req, res, next) => { console.log(req.originalUrl) next() } router.use(logger) ... // Various routes router.get(`/cars/:id`, verifyUsersJWTPassword, getCarDocument) ...
Identify the third-party Router-Level middleware that is used in the "MongoDB Embedded Documents" example in these notes.
We use the app.use() method to add middleware that will apply to all of the routes in the server/server.js file. In the "Cars" example, the `body-parser` and `cors` middleware are placed before the `/cars` and `/users` routers. Therefore, the `body-parser` and `cors` middleware will be called before all other `/cars` and `/users` middleware functions.
The Error 404 middleware matches every route. It is used to catch any route that has not already been caught by the `/cars` and `/users` routers. This must be placed after all other middleware and router code and before the error handling code.
The error handling middleware must be placed after all other middleware and router code.
// Server-side global variables require(`dotenv`).config({path:`./config/.env`}) // Database require(`./config/db`) // Express const express = require(`express`) const app = express() app.use(require(`body-parser`).json()) app.use(require(`cors`)({credentials: true, origin: process.env.LOCAL_HOST})) // Routers app.use(require(`./routes/cars`)) app.use(require(`./routes/users`)) // Port app.listen(process.env.SERVER_PORT, () => { console.log(`Connected to port ` + process.env.SERVER_PORT) }) // Error 404 app.use((req, res, next) => {next(createError(404))}) // Other errors app.use(function (err, req, res, next) { console.error(err.message) if (!err.statusCode) { err.statusCode = 500 } res.status(err.statusCode).send(err.message) })
Other than the middleware identified above, identify the third-party Application-Level middleware that was used in the "Server-side Sessions" example in these notes.
To include parameters in a middleware function, include the middleware function inside another function that accepts parameters and then returns the middleware based on these parameters. Usually, we add the parameters as a JSON object, as shown below:
const middlewareWithParameters = (options) => { return (req, res, next) => { // Implement the middleware function using the options JSON object parameter console.log(options.text) next() } }
The code above will just output a message to the server-side console. It can be used as shown below:
// Read one record router.get(`/cars/:id`, middlewareWithParameters({text:"This is an example of middleware that contains parameters"}), verifyUsersJWTPassword, getCarDocument)
Middleware can be placed in a separate file, which can be imported into an application. To place the middlewareWithParameters code above into a separate file called middlewareWithParameters.js, we place the code below in its own file. In this example, we shall call the file "middlewareFile.js" and we shall place it in the server/routes folder.
module.exports = (options) => { return (req, res, next) => { // Implement the middleware function using the options JSON object parameter console.log(options.text) next() } }
The middleware file can then be included in a router file, as shown below:
const middlewareWithParameters = require("middlewareFile.js")
The middlewareWithParameters middleware can be used exactly the same as in the previous code.
// Read one record router.get(`/cars/:id`, middlewareWithParameters({text:"This is an example of middleware that contains parameters"}), verifyUsersJWTPassword, getCarDocument)
Place the above "middlewareFile.js" file in the server/routes folder and use it in the file server/routes/cars.js router.get('/') method.
Error-handling middleware must include a fourth, error-handling, parameter, which is usually denoted as err. Even if we do not use the next object, we must still include it so that the middleware function can be identified as being error handling middleware. Error handling is usually included at the end of the server/server.js file, as shown below:
... app.use(function (err, req, res, next) { console.error(err.message) if (!err.statusCode) { err.statusCode = 500 } res.status(err.statusCode).send(err.message) })
We shall look at error handling in more detail in the next section of these notes.
Open the mongodb_embedded project from the previous section in these notes. Change the code to replace the router code with middleware functions.
The full project code for the "Cars" Worked Example that is described below can be downloaded from this link.
In this example, we replace the router code with middleware functions.
As middleware only affects server-side router code, it does not require any changes to the client-side code.
The users and cars router code has been modified, so that each route has been broken into several middleware functions.
const router = require(`express`).Router()
const usersModel = require(`../models/users`)
const bcrypt = require('bcrypt') // needed for password encryption
const jwt = require('jsonwebtoken')
const fs = require('fs')
const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILENAME, 'utf8')
const multer = require('multer')
const upload = multer({dest: `${process.env.UPLOADED_FILES_FOLDER}`})
const emptyFolder = require('empty-folder')
const checkThatUserExistsInUsersCollection = (req, res, next) =>
{
usersModel.findOne({email:req.params.email}, (error, data) =>
{
if(!data)
{
return res.json({errorMessage:`User is not logged in`})
}
req.data = data
return next()
})
}
const checkThatJWTPasswordIsValid = (req, res, next) =>
{
bcrypt.compare(req.params.password, req.data.password, (err, result) =>
{
if(!result)
{
return res.json({errorMessage:`User is not logged in`})
}
return next()
})
}
const checkThatFileIsUploaded = (req, res, next) =>
{
if(!req.file)
{
return res.json({errorMessage:`No file was selected to be uploaded`})
}
return next()
}
const checkThatFileIsAnImageFile = (req, res, next) =>
{
if(req.file.mimetype !== "image/png" && req.file.mimetype !== "image/jpg" && req.file.mimetype !== "image/jpeg")
{
fs.unlink(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, (error) => {return res.json({errorMessage:`Only .png, .jpg and .jpeg format accepted`})})
}
return next()
}
const checkThatUserIsNotAlreadyInUsersCollection = (req, res, next) =>
{
// If a user with this email does not already exist, then create new user
usersModel.findOne({email:req.params.email}, (uniqueError, uniqueData) =>
{
if(uniqueData)
{
return res.json({errorMessage:`User already exists`})
}
})
return next()
}
const addNewUserToUsersCollection = (req, res) =>
{
bcrypt.hash(req.params.password, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) =>
{
usersModel.create({name:req.params.name, email:req.params.email, password:hash, profilePhotoFilename:req.file.filename}, (error, data) =>
{
if(data)
{
const token = jwt.sign({email: data.email, accessLevel:data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY})
fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, 'base64', (err, fileData) =>
{
return res.json({name: data.name, accessLevel:data.accessLevel, profilePhoto:fileData, token:token})
})
}
else
{
return res.json({errorMessage:`User was not registered`})
}
})
})
}
const emptyUsersCollection = (req, res, next) =>
{
usersModel.deleteMany({}, (error, data) =>
{
if(error || !data)
{
return res.json({errorMessage:`User is not logged in`})
}
})
return next()
}
const addAdminUserToUsersCollection = (req, res) =>
{
const adminPassword = `123!"£qweQWE`
bcrypt.hash(adminPassword, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) =>
{
usersModel.create({name:"Administrator",email:"admin@admin.com",password:hash,accessLevel:parseInt(process.env.ACCESS_LEVEL_ADMIN)}, (createError, createData) =>
{
if(createData)
{
emptyFolder(process.env.UPLOADED_FILES_FOLDER, false, (result) =>
{
return res.json(createData)
})
}
else
{
return res.json({errorMessage:`Failed to create Admin user for testing purposes`})
}
})
})
}
const returnUsersDetailsAsJSON = (req, res) =>
{
const token = jwt.sign({email: req.data.email, accessLevel:req.data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY})
fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${req.data.profilePhotoFilename}`, 'base64', (err, fileData) =>
{
if(fileData)
{
return res.json({name: req.data.name, accessLevel:req.data.accessLevel, profilePhoto:fileData, token:token})
}
else
{
return res.json({name: req.data.name, accessLevel:req.data.accessLevel, profilePhoto:null, token:token})
}
})
}
const logout = (req, res) =>
{
return res.json({})
}
// IMPORTANT
// Obviously, in a production release, you should never have the code below, as it allows a user to delete a database collection
// The code below is for development testing purposes only
router.post(`/users/reset_user_collection`, emptyUsersCollection, addAdminUserToUsersCollection)
router.post(`/users/register/:name/:email/:password`, upload.single("profilePhoto"), checkThatFileIsUploaded, checkThatFileIsAnImageFile, checkThatUserIsNotAlreadyInUsersCollection, addNewUserToUsersCollection)
router.post(`/users/login/:email/:password`, checkThatUserExistsInUsersCollection, checkThatJWTPasswordIsValid, returnUsersDetailsAsJSON)
router.post(`/users/logout`, logout)
module.exports = router
All four routers have been replaced with multiple middleware.
const router = require(`express`).Router() const carsModel = require(`../models/cars`) const jwt = require('jsonwebtoken') const fs = require('fs') const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILENAME, 'utf8') const multer = require('multer') var upload = multer({dest: `${process.env.UPLOADED_FILES_FOLDER}`}) const verifyUsersJWTPassword = (req, res, next) => { jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => { if (err) { return res.json({errorMessage:`User is not logged in`}) } else { req.decodedToken = decodedToken return next() } }) } const checkThatUserIsAnAdministrator = (req, res, next) => { if(req.decodedToken.accessLevel >= process.env.ACCESS_LEVEL_ADMIN) { return next() } else { return res.json({errorMessage:`User is not an administrator`}) } } const createNewCarDocument = (req, res) => { // Use the new car details to create a new car document let carDetails = new Object() carDetails.model = req.body.model carDetails.colour = req.body.colour carDetails.year = req.body.year carDetails.price = req.body.price // add the car's photos to the carDetails JSON object carDetails.photos = [] req.files.map((file, index) => { carDetails.photos[index] = {filename:`${file.filename}`} }) carsModel.create(carDetails, (error, data) => { return res.json(data) }) } const getAllCarDocuments = (req, res) => { //user does not have to be logged in to see car details carsModel.find((error, data) => { return res.json(data) }) } const getCarPhotoAsBase64 = (req, res) => { fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${req.params.filename}`, 'base64', (err, fileData) => { if(fileData) { return res.json({image:fileData}) } else { return res.json({image:null}) } }) } const getCarDocument = (req, res) => { carsModel.findById(req.params.id, (error, data) => { return res.json(data) }) } const updateCarDocument = (req, res) => { carsModel.findByIdAndUpdate(req.params.id, {$set: req.body}, (error, data) => { return res.json(data) }) } const deleteCarDocument = (req, res) => { carsModel.findByIdAndRemove(req.params.id, (error, data) => { return res.json(data) }) } // read all records router.get(`/cars`, getAllCarDocuments) // get one car photo router.get(`/cars/photo/:filename`, getCarPhotoAsBase64) // Read one record router.get(`/cars/:id`, verifyUsersJWTPassword, getCarDocument) // Add new record router.post(`/cars`, verifyUsersJWTPassword, checkThatUserIsAnAdministrator, upload.array("carPhotos", parseInt(process.env.MAX_NUMBER_OF_UPLOAD_FILES_ALLOWED)), createNewCarDocument) // Update one record router.put(`/cars/:id`, verifyUsersJWTPassword, updateCarDocument) // Delete one record router.delete(`/cars/:id`, verifyUsersJWTPassword, checkThatUserIsAnAdministrator, deleteCarDocument) module.exports = router
All six routers have been replaced with multiple middleware.
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.