Middleware

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.

Function-level Middleware

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)

Router-Level Middleware

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.

Application-Level Middleware

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.

Middleware Parameters

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)

Third Party Middleware

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

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.

"Cars" Worked Example

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.

Client-Side

As middleware only affects server-side router code, it does not require any changes to the client-side code.

Server-Side

The users and cars router code has been modified, so that each route has been broken into several middleware functions.

server/routes/users.js

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.

server/routes/cars.js

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.

 
<div align="center"><a href="../versionC/index.html" title="DKIT Lecture notes homepage for Derek O&#39; Reilly, Dundalk Institute of Technology (DKIT), Dundalk, County Louth, Ireland. Copyright Derek O&#39; Reilly, DKIT." target="_parent" style='font-size:0;color:white;background-color:white'>&nbsp;</a></div>