Week 4
Middleware functions

Quiz 3: RESTful APIs 20 mins

There will be a quiz today. It will be worth 2% of your final grade.

Assignment Reminder

Assignment 1 - Basic CRUD - is due before next week's class.

Agenda

  • AMA (15 min)
  • Quiz (20 min)
  • Break (10 min)
  • What is middleware? (15 min)
  • Common use cases (5 min)
    • Built-in middleware
    • Community developed middleware
  • EX4-1 Router Module (30 mins)
  • Break (10 mins)
  • EX4-2 Project Folder Structure (10 mins)
  • EX4-3 carId Validator Middleware (30 mins)
  • Assignment 1: Basic CRUD (30 mins)

Review

The solution to last week's in-class exercise follows a similar pattern as the update methods. In fact the bulk of the code is a direct copy and paste. It is only the code in the else block that is different.
















 
 
 



app.delete('/api/cars/:carId', (req, res) => {
  const carId = parseInt(req.params.carId)
  const index = cars.findIndex(car => car.id === carId)
  if (index < 0) {
    res.status(404).send({
      errors: [
        {
          status: 'Not Found',
          code: '404',
          title: 'Resource does not exist',
          description: `We could not find a car with id: ${carId}`
        }
      ]
    })
  } else {
    const deletedCar = cars[index]
    cars.splice(index, 1)
    res.send({data: deletedCar})
  }
})

We save a copy of the car object that we are about to remove from the collection, so that we can send as confirmation in the response data. Then use the array.splice() method to cut out the element at index position index that we looked up in the earlier if block.

To slice() or to splice() ... that is the question.

Don't worry if you get these two array methods confused, most people do. When you're not sure, check the docs ...

The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.

The splice() method changes the contents of an array by removing or replacing existing elements and/or adding new elements.

What is middleware

Middleware functions are used to encapsulate functionality that you want to apply to multiple routes without repeating the code in every route handler. They run before the route handlers are evaluated and may be chained together. Each middleware function can either end the request or pass control to the next function in the pipeline, until the final route handler is reached.

middleware pipeline

From the docs ...

Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle. The next function is a function in the Express router which, when invoked, executes the middleware succeeding the current middleware.

Middleware functions can perform the following tasks:

  • Execute any code.
  • Make changes to the request and the response objects.
  • End the request-response cycle.
  • Call the next middleware in the stack.

Middleware function signature

const myMiddleware = (req, res, next) => {
  // validate something
  // if fail, return error to client
  // if OK, call next()
}

Types of middleware

An Express application can use the following types of middleware:

  • Application-level middleware
  • Router-level middleware
  • Error-handling middleware
    • different function signature
  • Built-in middleware
    • express.json()
    • express.urlencoded()
    • express.static
  • Third-party middleware

Common use cases

  • router modules
  • validation
  • error handling
  • image handling
  • enforcing security best practices

Timestamp Logger

This is a simple example of a middleware function that logs the requested resource path and the time of the request. It is invoked on the main express app with no route qualifier, so it will apply to all incoming HTTP requests.

const express = require('express')
const app = express()

const timestampLogger = (req, res, next) => {
  console.log(`${req.path} requested at ${Date.now()}`)
  next()
}

app.use(timestampLogger)

This was a trivial example of an access log middleware. A more production ready solution can be found in the popular third-party module called morgan. It is easily added to your project like this ...

npm install morgan

 



 



// app.js
const morgan = require('morgan')
const express = require('express')
const app = express()

app.use(morgan('tiny'))

// rest of your app config.

Router modules

A special kind of middleware is the Router Module.

A router object is an isolated instance of middleware and routes. You can think of it as a “mini-application,” capable only of performing middleware and routing functions. Every Express application has a built-in app router.

A router behaves like middleware itself, so you can use it as an argument to app.use() or as the argument to another router’s use() method.

EX4-1 Router Module

Objective

Move all of the existing route handler methods (like app.get or app.post) from the main app.js file to a separate carsRouter.js Router module.

Picking up from last week's Express CRUD exercise, accept this GitHub Classroom assignment and clone the resulting repository to the code folder for this course on your laptop.

Install dependencies

This starter repo has our dependencies defined in the package.json file. Before we go any further, we need to run npm install in the terminal of the project folder to download them.

We will refactor the Express CRUD RESTful API routes into a separate Router module.

Create a new file in our project folder called carsRouter.js and create a new express.Router object.

const express = require('express')
const router = express.Router()

This router will have all of the same HTTP verb methods that our main app object has. So, when we copy the methods over from app.js, substitute router for app like this ...

// old code
app.get('/api/cars', (req, res) => res.send({data: cars}))

// becomes new code
router.get('/', (req, res) => res.send({data: cars}))

Notice that the route path above is just /, whereas it was /api/cars before. That is because all of the resource paths in these route handler methods are relative to the root path for the resource which we need to set back in our app.js file when we tell our express app to use this router, and that will be /api/cars.

Similarly, route paths with route parameters will look like this ...

 





router.get('/:carId', (req, res) => {
  const carId = parseInt(req.params.carId)
  const car = cars.find(car => car.id === carId)
  res.send({data: car})
})

Your task

Update the rest of the route handler methods accordingly.

Don't forget, we need to export our router object from this module. Add the module.exports line at the bottom of the carsRouter.js file.

module.exports = router

Now, back in app.js, we need to import the new carsRouter object and tell the express app object to use it for routes starting with /api/cars

App.js should now look like this ...





 


 




'use strict'

const express = require('express')
const app = express()
const carsRouter = require('./carsRouter.js')

app.use(express.json())
app.use('/api/cars', carsRouter)

const port = process.env.port || 3030
app.listen(port, () => console.log(`Server listening on port ${port} ...`))

Notice that we are no longer importing the cars collection data in app.js. This should be private data in our carsRouter module. Let's load it at the top of the carsRouter.js file.

 



const cars = require('./cars.js')
const express = require('express')
const router = express.Router()

That should do it! Let's test it with Postman.

Run a full set of tests on all of the six /api/cars resource routes.

Commit our work

If everything is working, create a new git commit with the message, "Refactor /api/cars routes into a dedicated router module."

git add .
git commit -m "Refactor /api/cars routes into a dedicated router module."

EX4-2 Project Folder Structure

Let's get organized

Its time to start organizing our project folder structure.

As our application grows in features and complexity, it is a good practice to organize the various modules of our application into sub-folders. This makes it much easier to navigate and find the correct module when we need to make changes.

To get started, create a new routes folder and move the carsRouter.js file into that new folder. The common practice is to name the files in the routes folder by the name of the resource path. So, rename carsRouter.js to cars.js.

Now create a new folder called data and move the cars.json file into that new folder.

Create one more folder called middleware. We will add our custom middleware modules here starting with the validateId.js that we will create in the next section.

Don't forget to update the relative paths for your require() functions to point to the correct sub-folders. e.g.

// in the /routes/cars.js file
const cars = require('../data/cars.json')

// in the app.js file
const carsRouter = require('./routes/cars.js')

Our reorganized project file structure should now look like this ...

.
├── app.js
├── package.json
├── data
│   └── cars.json
├── middleware
│   └── validateId.js
└── routes
    └── cars.js

Test and Commit

Test all of the routes with Postman, and if everything is still working, create a new git commit.

git add .
git commit -m "Refactor folder structure."

EX4-3 carId Validator Middleware

Objective

Instead of repeating our block of code to check for a valid carId on every route, we can move that to a middleware function and greatly simplify our implementation logic.

Create a new file called validateCarId.js in the middleware folder. Then scaffold out the basic signature for a middleware module. Remember, it is almost the same as the route handler callback functions, but has a third argument - the next() function.

We will also need access to the cars collection, so require the cars.json file at the top of our middleware module.

const cars = require('../data/cars.json')
const validateCarId = (req, res, next) => {}

module.exports = validateCarId

OK, now we can copy the carId validation logic from one of our route handlers and paste it into the validateCarId function. Most of it can be copied unchanged. Only the else block needs to be updated. If the carId was valid, we want to make the index position available to our route handlers as a property on the request object - so that it does not have to be looked up again.

Simply declare a new property on the req object and assign the index value to it.

req.carIndex = index

The last thing to do is call the next() method, to tell express that it can move on to the next middleware function or route handler.

The complete validateCarId.js middleware module should now look like this.


















 
 





const cars = require('../data/cars.json')

const validateCarId = (req, res, next) => {
  const carId = parseInt(req.params.carId)
  const index = cars.findIndex(car => car.id === carId)
  if (index < 0) {
    res.status(404).send({
      errors: [
        {
          status: 'Not Found',
          code: '404',
          title: 'Resource does not exist',
          description: `We could not find a car with id: ${carId}`
        }
      ]
    })
  } else {
    req.carIndex = index
    next()
  }
}

module.exports = validateCarId

To use our new custom middleware function, we need to require it in our cars router module and then tell the router object to use our middleware on all routes with a :carId route parameter.

The top of /router/cars.js should look like this ...


 



 

const cars = require('../data/cars.json')
const validateCarId = require('../middleware/validateCarId')
const express = require('express')
const router = express.Router()

router.use('/:carId', validateCarId)

We can remove the now redundant carId validation checks in the get, put, patch and delete functions. Since we no longer have a local index variable in these functions, we need to make some minor updates to utilize the req.carIndex property that our middleware function created.

router.get('/:carId', (req, res) => res.send({data: cars[req.carIndex]}))

router.put('/:carId', (req, res) => {
  const {make, model, colour} = req.body
  const updatedCar = {id: parseInt(req.params.carId), make, model, colour}
  cars[req.carIndex] = updatedCar
  res.send({data: updatedCar})
})

router.patch('/:carId', (req, res) => {
  const {id, ...theRest} = req.body
  const updatedCar = Object.assign({}, cars[req.carIndex], theRest)
  cars[req.carIndex] = updatedCar
  res.send({data: updatedCar})
})

router.delete('/:carId', (req, res) => {
  const deletedCar = cars[req.carIndex]
  cars.splice(req.carIndex, 1)
  res.send({data: deletedCar})
})

Test and Commit

Test all of the routes with Postman, and if everything is still working, create a new git commit.

git add .
git commit -m "Refactor cars router to use carId validator middleware."

Push your commits up to the remote GitHub repo, and submit the GitHub repo's URL on BrightSpace.

For next week

Assignment Reminder

Assignment 1 - Basic CRUD - is due before 1:00 pm on Friday, February 7th.

Before next week's class, please read these additional online resources.

Quiz

There will be a short quiz next class. The questions could come from any of the material referenced above.

Last Updated: 1/5/2020, 5:05:07 PM