Chapter 10. Middleware

By now, we’ve already had some exposure to middleware: we’ve used existing middleware (body-parser, cookie-parser, static, and express-session, to name a few), and we’ve even written some of our own (for adding weather data to our template context, configuring flash messages, and our 404 handler). But what is middleware, exactly?

Conceptually, middleware is a way to encapsulate functionality—specifically, functionality that operates on an HTTP request to your application. Practically, middleware is simply a function that takes three arguments: a request object, a response object, and a next() function, which will be explained shortly. (There is also a form that takes four arguments, for error handling, which will be covered at the end of this chapter.)

Middleware is executed in what’s known as a pipeline. You can imagine a physical pipe, carrying water. The water gets pumped in at one end, and then there are gauges and valves before the water gets where it’s going. The important part about this analogy is that order matters; if you put a pressure gauge before a valve, it has a different effect than if you put the pressure gauge after the valve. Similarly, if you have a valve that injects something into the water, everything “downstream” from that valve will contain the added ingredient. In an Express app, you insert middleware into the pipeline by calling app.use.

Prior to Express 4.0, the pipeline was complicated by your having to link in the router explicitly. Depending on where you linked in the router, routes could be linked in out of order, making the pipeline sequence less clear when you mix middleware and route handlers. In Express 4.0, middleware and route handlers are invoked in the order in which they were linked in, making the sequence much clearer.

It’s common practice to have the last middleware in your pipeline be a catchall handler for any request that doesn’t match any other routes. This middleware usually returns a status code of 404 (Not Found).

So how is a request “terminated” in the pipeline? That’s what the next function passed to each middleware does: if you don’t call next(), the request terminates with that middleware.

Middleware Principles

Learning how to think flexibly about middleware and route handlers is key to understanding how Express works. Here are the things you should keep in mind:

  • Route handlers (app.get, app.post, etc.—often referred to collectively as app.METHOD) can be thought of as middleware that handles only a specific HTTP verb (GET, POST, etc.). Conversely, middleware can be thought of as a route handler that handles all HTTP verbs (this is essentially equivalent to app.all, which handles any HTTP verb; there are some minor differences with exotic verbs such as PURGE, but for the common verbs, the effect is the same).

  • Route handlers require a path as their first parameter. If you want that path to match any route, simply use *. Middleware can also take a path as its first parameter, but it is optional (if it is omitted, it will match any path, as if you had specified *).

  • Route handlers and middleware take a callback function that takes two, three, or four parameters (technically, you could also have zero or one parameters, but there is no sensible use for these forms). If there are two or three parameters, the first two parameters are the request and response objects, and the third parameter is the next function. If there are four parameters, it becomes error-handling middleware, and the first parameter becomes an error object, followed by the request, response, and next objects.

  • If you don’t call next(), the pipeline will be terminated, and no more route handlers or middleware will be processed. If you don’t call next(), you should send a response to the client (res.send, res.json, res.render, etc.); if you don’t, the client will hang and eventually time out.

  • If you do call next(), it’s generally inadvisable to send a response to the client. If you do, middleware or route handlers further down the pipeline will be executed, but any client responses they send will be ignored.

Middleware Examples

If you want to see this in action, let’s try some really simple middleware (ch10/00-simple-middleware.js in the companion repo):

app.use((req, res, next) => {
  console.log(`processing request for ${req.url}....`)
  next()
})

app.use((req, res, next) => {
  console.log('terminating request')
  res.send('thanks for playing!')
  // note that we do NOT call next() here...this terminates the request
})

app.use((req, res, next) => {
  console.log(`whoops, i'll never get called!`)
})

Here we have three examples of middleware. The first one simply logs a message to the console before passing on the request to the next middleware in the pipeline by calling next(). Then the next middleware actually handles the request. Note that if we omitted the res.send here, no response would ever be returned to the client. Eventually, the client would time out. The last middleware will never execute, because all requests are terminated in the prior middleware.

Now let’s consider a more complicated, complete example (ch10/01-routing-example.js in the companion repo):

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

app.use((req, res, next) => {
  console.log('

ALLWAYS')
  next()
})

app.get('/a', (req, res) => {
  console.log('/a: route terminated')
  res.send('a')
})
app.get('/a', (req, res) => {
  console.log('/a: never called');
})
app.get('/b', (req, res, next) => {
  console.log('/b: route not terminated')
  next()
})
app.use((req, res, next) => {
  console.log('SOMETIMES')
  next()
})
app.get('/b', (req, res, next) => {
  console.log('/b (part 2): error thrown' )
  throw new Error('b failed')
})
app.use('/b', (err, req, res, next) => {
  console.log('/b error detected and passed on')
  next(err)
})
app.get('/c', (err, req) => {
  console.log('/c: error thrown')
  throw new Error('c failed')
})
app.use('/c', (err, req, res, next) => {
  console.log('/c: error detected but not passed on')
  next()
})

app.use((err, req, res, next) => {
  console.log('unhandled error detected: ' + err.message)
  res.send('500 - server error')
})

app.use((req, res) => {
  console.log('route not handled')
  res.send('404 - not found')
})

const port = process.env.PORT || 3000
app.listen(port, () => console.log( `Express started on http://localhost:${port}` +
  '; press Ctrl-C to terminate.'))

Before trying this example, imagine what the result will be. What are the different routes? What will the client see? What will be printed on the console? If you can correctly answer all of those questions, you’ve got the hang of routes in Express! Pay particular attention to the difference between a request to /b and a request to /c; in both instances, there was an error, but one results in a 404, and the other results in a 500.

Note that middleware must be a function. Keep in mind that in JavaScript, it’s quite easy (and common) to return a function from a function. For example, you’ll note that express.static is a function, but we actually invoke it, so it must return another function. Consider the following:

app.use(express.static)         // this will NOT work as expected

console.log(express.static())   // will log "function", indicating
                                // that express.static is a function
                                // that itself returns a function

Note also that a module can export a function, which can in turn be used directly as middleware. For example, here’s a module called lib/tourRequiresWaiver.js (Meadowlark Travel’s rock-climbing packages require a liability waiver):

module.exports = (req,res,next) => {
  const { cart } = req.session
  if(!cart) return next()
  if(cart.items.some(item => item.product.requiresWaiver)) {
    cart.warnings.push('One or more of your selected ' +
      'tours requires a waiver.')
  }
  next()
}

We could link this middleware in like so (ch10/02-item-waiver.example.js in the companion repo):

const requiresWaiver = require('./lib/tourRequiresWaiver')
app.use(requiresWaiver)

More commonly, though, you would export an object that contains properties that are middleware. For example, let’s put all of our shopping cart validation code in lib/cartValidation.js:

module.exports = {

  resetValidation(req, res, next) {
    const { cart } = req.session
    if(cart) cart.warnings = cart.errors = []
    next()
  },

  checkWaivers(req, res, next) {
    const { cart } = req.session
    if(!cart) return next()
    if(cart.items.some(item => item.product.requiresWaiver)) {
      cart.warnings.push('One or more of your selected ' +
        'tours requires a waiver.')
    }
    next()
  },

  checkGuestCounts(req, res, next) {
    const { cart } = req.session
    if(!cart) return next()
    if(cart.items.some(item => item.guests > item.product.maxGuests )) {
      cart.errors.push('One or more of your selected tours ' +
        'cannot accommodate the number of guests you ' +
        'have selected.')
    }
    next()
  },

}

Then you could link the middleware in like this (ch10/03-more-cart-validation.js in the companion repo):

const cartValidation = require('./lib/cartValidation')

app.use(cartValidation.resetValidation)
app.use(cartValidation.checkWaivers)
app.use(cartValidation.checkGuestCounts)
Note

In the previous example, we have middleware aborting early with the statement return next(). Express doesn’t expect middleware to return a value (and it doesn’t do anything with any return values), so this is just a shortened way of writing next(); return.

Common Middleware

While there are thousands of middleware projects on npm, there are a handful that are common and fundamental, and at least some of these will be found in every non-trivial Express project. Some of this middleware was so common that it was actually bundled with Express, but it has long since been moved into individual packages. The only middleware still bundled with Express itself is static.

This list attempts to cover the most common middleware:

basicauth-middleware

Provides basic access authorization. Keep in mind that basic auth offers only the most basic security, and you should use basic auth only over HTTPS (otherwise, usernames and passwords are transmitted in the clear). You should use basic auth only when you need something quick and easy and you’re using HTTPS.

body-parser

Provides parsing for HTTP request bodies. Provides middleware for parsing both URL-encoded and JSON-encoded bodies, as well as others.

busboy, multiparty, formidable, multer

All of these middleware options parse request bodies encoded with multipart/form-data.

compression

Compresses response data with gzip or deflate. This is a good thing, and your users will thank you, especially those on slow or mobile connections. It should be linked in early, before any middleware that might send a response. The only thing that I recommend linking in before compress is debugging or logging middleware (which do not send responses). Note that in most production environments, compression is handled by a proxy like NGINX, making this middleware unnecessary.

cookie-parser

Provides cookie support. See Chapter 9.

cookie-session

Provides cookie-storage session support. I do not generally recommend this approach to sessions. It must be linked in after cookie-parser. See Chapter 9.

express-session

Provides session ID (stored in a cookie) session support. Defaults to a memory store, which is not suitable for production and can be configured to use a database store. See Chapter 9 and Chapter 13.

csurf

Provides protection against cross-site request forgery (CSRF) attacks. This uses sessions, so it must be linked in after express-session middleware. Unfortunately, simply linking in this middleware does not magically protect against CSRF attacks; see Chapter 18 for more information.

serve-index

Provides directory listing support for static files. There is no need to include this middleware unless you specifically need directory listing.

errorhandler

Provides stack traces and error messages to the client. I do not recommend linking this in on a production server, as it exposes implementation details, which can have security or privacy consequences. See Chapter 20 for more information.

serve-favicon

Serves the favicon (the icon that appears in the title bar of your browser). This is not strictly necessary; you can simply put a favicon.ico in the root of your static directory, but this middleware can improve performance. If you use it, it should be linked in high in the middleware stack. It also allows you to designate a filename other than favicon.ico.

morgan

Provides automated logging support; all requests will be logged. See Chapter 20 for more information.

method-override

Provides support for the x-http-method-override request header, which allows browsers to “fake” using HTTP methods other than GET and POST. This can be useful for debugging. This is needed only if you’re writing APIs.

response-time

Adds the X-Response-Time header to the response, providing the response time in milliseconds. You usually don’t need this middleware unless you are doing performance tuning.

static

Provides support for serving static (public) files. You can link in this middleware multiple times, specifying different directories. See Chapter 17 for more details.

vhost

Virtual hosts (vhosts), a term borrowed from Apache, makes subdomains easier to manage in Express. See Chapter 14 for more information.

Third-Party Middleware

Currently, there is no comprehensive “store” or index for third-party middleware. Almost all Express middleware, however, will be available on npm, so if you search npm for “Express” and “middleware,” you’ll get a pretty good list. The official Express documentation also contains a useful list of middleware.

Conclusion

In this chapter, we delved into what middleware is, how to write our own, and how it’s processed as part of an Express application. If you’re starting to think that an Express application is simply a collection of middleware, you’re starting to understand Express! Even the route handlers we’ve been using heretofore are just specialized cases of middleware.

In the next chapter, we’ll be looking at another common infrastructure need: sending email (and you had better believe there is going to be some middleware involved!).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.147.87