0% completed
Understanding Middleware in Express.js
On This Page
A Request Through the Stack
Order Is Part of the Program
Where a Middleware Function Applies
Where Middleware Functions Come From
Error-Handling Middleware
Express can run a series of functions for one request before the route handler gets it. Each of those functions receives the request, the response, and a function that hands the request to the next step in the series. A function of this kind is called a middleware function, and the series of middleware functions registered on an app is called the middleware stack.
Every middleware function has the same signature:
function name(req, res, next) { // read or change req and res here next(); // or send a response instead }
Inside the function, req and res are the same objects that the route handler later receives, so a change that one middleware function makes is visible to every function after it. The third argument, next, is a function that Express provides. Calling it tells Express to continue with the next function in the stack. A middleware function that does not call next() must send a response itself, because otherwise the request stops there and the client waits.
A Request Through the Stack
The task API below registers three middleware functions before its routes. The first prints the request line, the second rejects a POST request whose body is not JSON, and the third is the JSON body parser from Express:
const express = require('express'); const app = express(); const tasks = [ { id: 1, title: 'Write the README', done: true }, { id: 2, title: 'Buy milk', done: false }, ]; function requestLogger(req, res, next) { console.log(`requestLogger: ${req.method} ${req.url}`); next(); } function requireJson(req, res, next) { console.log('requireJson: checking the body type'); if (req.method === 'POST' && !req.is('application/json')) { return res.status(415).json({ error: 'Send the task as JSON' }); } next(); } app.use(requestLogger); app.use(requireJson); app.use(express.json()); app.get('/tasks', (req, res) => { console.log('route handler: GET /tasks'); res.json(tasks); }); app.post('/tasks', (req, res) => { console.log('route handler: POST /tasks'); const task = { id: tasks.length + 1, title: req.body.title, done: false }; tasks.push(task); res.status(201).json(task); }); app.listen(3000);
The method req.is() checks the Content-Type header of the request against a type, and the status 415 Unsupported Media Type tells the client that the server does not accept that kind of body. Express keeps the functions in the order of the app.use() and app.get() calls. For each request, it starts at the top of that list and moves down one step every time a function calls next().
A POST /tasks request with the header Content-Type: application/json and the body {"title":"Buy bread"} passes through all three middleware functions and reaches its route, which answers 201 Created with {"id":3,"title":"Buy bread","done":false}. The server prints:
requestLogger: POST /tasks requireJson: checking the body type route handler: POST /tasks
The same request sent with the header Content-Type: text/plain takes a shorter path. The function requireJson sends a 415 response and does not call next(), so the body parser and the route handler never run:
requestLogger: POST /tasks requireJson: checking the body type
HTTP/1.1 415 Unsupported Media Type Content-Type: application/json; charset=utf-8 {"error":"Send the task as JSON"}
Follow the two arrows in the diagram from the top. Both requests enter at requestLogger, and each arrow continues downward only where a function calls next(). A middleware function that answers early, as requireJson does here, ends the cycle for that request.
Order Is Part of the Program
Because Express walks the stack from top to bottom, the position of app.use() changes what the program does. If you move app.use(requestLogger) below the two routes, a GET /tasks request no longer prints the requestLogger line. The route handler sends the response and does not call next(), so Express never reaches the logger.
A request for a path that no route handles, such as GET /nothing, still reaches the logger in that position, because no route ended its cycle first. The same rule explains why express.json() must come before the routes that read req.body: a parser registered after a route runs too late for that route.
Where a Middleware Function Applies
A middleware function can apply to every request, to a group of paths, or to a single route. The place where you register it decides which one.
Every request. app.use(fn) with no path runs fn for every request, as requestLogger does above. Middleware registered on the app object is called application-level middleware.
A path and everything below it. app.use('/tasks', fn) runs fn for /tasks and for any path that continues with a slash, such as /tasks/2. It does not run for /taskslist, because the path must match whole segments. Inside fn, req.url has the mount path removed, so for /tasks/2 it is /2, while req.originalUrl keeps the full /tasks/2.
One route. A function placed between the path and the handler, as in app.get('/tasks', fn, handler), runs only for that route. The lesson on custom middleware later in this chapter uses this form to protect a single route.
A router. A router is a small, separate stack of middleware and routes, created with express.Router(), that you mount on a path of the app. Middleware registered on a router is called router-level middleware, and it runs only for requests that reach that router:
const adminRouter = express.Router(); adminRouter.use((req, res, next) => { console.log(`adminRouter: ${req.method} ${req.originalUrl} (req.url is ${req.url})`); next(); }); adminRouter.get('/stats', (req, res) => { res.json({ taskCount: tasks.length }); }); app.use('/admin', adminRouter);
A GET /admin/stats request prints adminRouter: GET /admin/stats (req.url is /stats) and receives {"taskCount":2}. A GET /tasks request never enters the router, so the router's middleware does not run for it. Chapter 3 uses routers to split a large app into files.
Where Middleware Functions Come From
You will use middleware from three sources, and all of them follow the same (req, res, next) signature.
Built into Express. express.json(), express.urlencoded(), express.text(), express.raw(), and express.static() ship with Express, and the next lesson covers each one.
Written by you. Functions such as requestLogger and requireJson are custom middleware. A later lesson in this chapter shows how to write them for timing, authentication, and loading data.
Installed from npm. Third-party packages provide middleware for common needs. One example is morgan, which prints one line for each finished request. Install it with npm install morgan, and register it at the top of the stack:
const morgan = require('morgan'); app.use(morgan('dev'));
With the dev format, morgan prints the method, the path, the status code, the time the server took, and the size of the response body in bytes. For a GET /tasks request, the rejected text request, and a JSON request that adds the task Buy bread, it printed these lines:
GET /tasks 200 3.119 ms - 90 POST /tasks 415 0.760 ms - 33 POST /tasks 201 16.303 ms - 41
Error-Handling Middleware
One more kind of middleware handles errors instead of requests. An error-handling middleware function takes four parameters, (err, req, res, next), and Express calls it only when an earlier function has failed. That happens when a handler throws an error or passes an error to next(err). Register it after all routes, so that it sits below everything that might fail:
app.get('/broken', (req, res) => { throw new Error('The task list is not available'); }); app.use((err, req, res, next) => { console.error(`errorHandler: ${err.message}`); res.status(500).json({ error: 'Something went wrong' }); });
A request to /broken skips every remaining normal middleware function and route, reaches the error handler, and gets the status 500 with the body {"error":"Something went wrong"}. The client sees a short message, while the server log keeps the details.
Express recognizes an error handler only by its four parameters. If you leave out next because the function does not use it, Express treats the function as normal middleware and skips it when an error occurs. In a test of that mistake, the error went to the built-in Express handler, which sent its HTML error page instead of the JSON body. Chapter 3 covers error handling in routes in more depth.
Reading Progress
0%
On This Page
A Request Through the Stack
Order Is Part of the Program
Where a Middleware Function Applies
Where Middleware Functions Come From
Error-Handling Middleware