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 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.