Notes Node JS
Projects:
Middleware
Software that acts as a bridge between an operating system or database and applications, especially on a network. Middleware is a type of computer software that provides services to software applications beyond those available from the operating system. It can be described as "software glue".
With Regard to Express
Express is a routing and middleware web framework that has minimal functionality of its own: An Express application is essentially a series of middleware function calls.
Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle. The next middleware function is commonly denoted by a variable named next.
Types of Middleware
An Express application can use the following types of middleware:
- Application-Level Middleware
- Router-Level Middleware
- Error-Handling Middleware
- Built-In Middleware
- Third-Party Middleware
You can load application-level and router-level middleware with an optional mount path. You can also load a series of middleware functions together, which creates a sub-stack of the middleware system at a mount point.
Application-Level Middleware
Bind application-level middleware to an instance of the app object by using the app.use() and app.METHOD() functions, where METHOD is the HTTP method of the request that the middleware function handles (such as GET, PUT, or POST) in lowercase.
This example shows a middleware function with no mount path. The function is executed every time the app receives a request.
const express = require('express') const app = express() app.use((req, res, next) => { console.log('Time:', Date.now()) next() })
This example shows a middleware function mounted on the /user/:id path. The function is executed for any type of HTTP request on the /user/:id path.
app.use('/user/:id', (req, res, next) => { console.log('Request Type:', req.method) next() })
This example shows a route and its handler function (middleware system). The function handles GET requests to the /user/:id path.
app.get('/user/:id', (req, res, next) => { res.send('USER') })
Here is an example of loading a series of middleware functions at a mount point, with a mount path.
It illustrates a middleware sub-stack that prints request info for any type of HTTP request to the /user/:id path.
app.use('/user/:id', (req, res, next) => { console.log('Request URL:', req.originalUrl) next() }, (req, res, next) => { console.log('Request Type:', req.method) next() })
Route handlers enable you to define multiple routes for a path. The example below defines two routes for GET requests to the /user/:id path. The second route will not cause any problems, but it will never get called because the first route ends the request-response cycle.
This example shows a middleware sub-stack that handles GET requests to the /user/:id path.
app.get('/user/:id', (req, res, next) => { console.log('ID:', req.params.id) next() }, (req, res, next) => { res.send('User Info') }) // handler for the /user/:id path, which prints the user ID app.get('/user/:id', (req, res, next) => { res.send(req.params.id) })
Middleware can also be declared in an array for reusability.
This example shows an array with a middleware sub-stack that handles GET requests to the /user/:id path
function logOriginalUrl (req, res, next) { console.log('Request URL:', req.originalUrl) next() } function logMethod (req, res, next) { console.log('Request Type:', req.method) next() } const logStuff = [logOriginalUrl, logMethod] app.get('/user/:id', logStuff, (req, res, next) => { res.send('User Info') })
Router-Level Middleware
Router-level middleware works in the same way as application-level middleware, except it is bound to an instance of express.Router().
const router = express.Router();
Load router-level middleware by using the router.use() and router.METHOD() functions.
The following example code replicates the middleware system that is shown above for application-level middleware, by using router-level middleware:
const express = require('express') const app = express() const router = express.Router() // a middleware function with no mount path. This code is executed for every request to the router router.use((req, res, next) => { console.log('Time:', Date.now()) next() }) // a middleware sub-stack shows request info for any type of HTTP request to the /user/:id path router.use('/user/:id', (req, res, next) => { console.log('Request URL:', req.originalUrl) next() }, (req, res, next) => { console.log('Request Type:', req.method) next() }) // a middleware sub-stack that handles GET requests to the /user/:id path router.get('/user/:id', (req, res, next) => { // if the user ID is 0, skip to the next router if (req.params.id === '0') next('route') // otherwise pass control to the next middleware function in this stack else next() }, (req, res, next) => { // render a regular page res.render('regular') }) // handler for the /user/:id path, which renders a special page router.get('/user/:id', (req, res, next) => { console.log(req.params.id) res.render('special') }) // mount the router on the app app.use('/', router)
To skip the rest of the router's middleware functions, call next('router') to pass control back out of the router instance.
This example shows a middleware sub-stack that handles GET requests to the /user/:id path.
const express = require('express') const app = express() const router = express.Router() // predicate the router with a check and bail out when needed router.use((req, res, next) => { if (!req.headers['x-auth']) return next('router') next() }) router.get('/user/:id', (req, res) => { res.send('hello, user!') }) // use the router and 401 anything falling through app.use('/admin', router, (req, res) => { res.sendStatus(401) })
Error-Handling Middleware
Note: Error-handling middleware always takes four arguments.
You must provide four arguments to identify it as an error-handling middleware function.
Even if you don't need to use the next object,
you must specify it to maintain the signature.
Otherwise, the next object will be interpreted as regular middleware and will fail to handle errors.
Define error-handling middleware functions in the same way as other middleware functions, except with four arguments instead of three,
specifically with the signature (err, req, res, next)
app.use((err, req, res, next) => { console.error(err.stack) res.status(500).send('Something broke!') })
Catching Errors
It's important to ensure that Express catches all errors that occur while running route handlers and middleware.
Errors that occur in synchronous code inside route handlers and middleware require no extra work. If synchronous code throws an error, then Express will catch and process it. For example:
app.get('/', (req, res) => { throw new Error('BROKEN') // Express will catch this on its own. })
For errors returned from asynchronous functions invoked by route handlers and middleware, you must pass them to the next() function, where Express will catch and process them. For example:
app.get('/', (req, res, next) => { fs.readFile('/file-does-not-exist', (err, data) => { if (err) { next(err) // Pass errors to Express. } else { res.send(data) } }) })
Starting with Express 5, route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error. For example:
app.get('/user/:id', async (req, res, next) => { const user = await getUserById(req.params.id) res.send(user) })
If getUserById throws an error or rejects, next will be called with either the thrown error or the rejected value. If no rejected value is provided, next will be called with a default Error object provided by the Express router.
If you pass anything to the next() function (except the string 'route'), Express regards the current request as being an error and will skip any remaining non-error handling routing and middleware functions.
If the callback in a sequence provides no data, only errors, you can simplify this code as follows:
app.get('/', [ function (req, res, next) { fs.writeFile('/inaccessible-path', 'data', next) }, function (req, res) { res.send('OK') } ])
In the above example next is provided as the callback for fs.writeFile, which is called with or without errors. If there is no error the second handler is executed, otherwise Express catches and processes the error.
You must catch errors that occur in asynchronous code invoked by route handlers or middleware and pass them to Express for processing. For example:
app.get('/', (req, res, next) => { setTimeout(() => { try { throw new Error('BROKEN') } catch (err) { next(err) } }, 100) })
The above example uses a try...catch block to catch errors in the asynchronous code and pass them to Express. If the try...catch block were omitted, Express would not catch the error since it is not part of the synchronous handler code.
Use promises to avoid the overhead of the try...catch block or when using functions that return promises. For example:
app.get('/', (req, res, next) => { Promise.resolve().then(() => { throw new Error('BROKEN') }).catch(next) // Errors will be passed to Express. })
Since promises automatically catch both synchronous errors and rejected promises, you can simply provide next as the final catch handler and Express will catch errors, because the catch handler is given the error as the first argument.
You could also use a chain of handlers to rely on synchronous error catching, by reducing the asynchronous code to something trivial. For example:
app.get('/', [ function (req, res, next) { fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => { res.locals.data = data next(err) }) }, function (req, res) { res.locals.data = res.locals.data.split(',')[1] res.send(res.locals.data) } ])
The above example has a couple of trivial statements from the readFile call. If readFile causes an error, then it passes the error to Express, otherwise you quickly return to the world of synchronous error handling in the next handler in the chain. Then, the example above tries to process the data. If this fails then the synchronous error handler will catch it. If you had done this processing inside the readFile callback then the application might exit and the Express error handlers would not run.
Whichever method you use, if you want Express error handlers to be called in and the application to survive, you must ensure that Express receives the error.
The Default Error Handler
Express comes with a built-in error handler that takes care of any errors that might be encountered in the app. This default error-handling middleware function is added at the end of the middleware function stack.
If you pass an error to next() and you do not handle it in a custom error handler, it will be handled by the built-in error handler; the error will be written to the client with the stack trace. The stack trace is not included in the production environment.
Set the environment variable NODE_ENV to production, to run the app in production mode.
When an error is written, the following information is added to the response:
- The res.statusCode is set from err.status (or err.statusCode). If this value is outside the 4xx or 5xx range, it will be set to 500.
- The res.statusMessage is set according to the status code.
- The body will be the HTML of the status code message when in production environment, otherwise will be err.stack.
- Any headers specified in an err.headers object.
- If you call next() with an error after you have started writing the response (for example, if you encounter an error while streaming the response to the client) the Express default error handler closes the connection and fails the request.
So when you add a custom error handler, you must delegate to the default Express error handler, when the headers have already been sent to the client:
function errorHandler (err, req, res, next) { if (res.headersSent) { return next(err) } res.status(500) res.render('error', { error: err }) }
Note that the default error handler can get triggered if you call next() with an error in your code more than once, even if custom error handling middleware is in place.
Writing Error Handlers
Define error-handling middleware functions in the same way as other middleware functions, except error-handling functions have four arguments instead of three: (err, req, res, next). For example:
app.use((err, req, res, next) => { console.error(err.stack) res.status(500).send('Something broke!') })
You define error-handling middleware last, after other app.use() and routes calls; for example:
const bodyParser = require('body-parser') const methodOverride = require('method-override') app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.json()) app.use(methodOverride()) app.use((err, req, res, next) => { // logic })
Responses from within a middleware function can be in any format, such as an HTML error page, a simple message, or a JSON string.
For organizational (and higher-level framework) purposes, you can define several error-handling middleware functions, much as you would with regular middleware functions. For example, to define an error-handler for requests made by using XHR and those without:
const bodyParser = require('body-parser') const methodOverride = require('method-override') app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.json()) app.use(methodOverride()) app.use(logErrors) app.use(clientErrorHandler) app.use(errorHandler)
In this example, the generic logErrors might write request and error information to stderr, for example:
function logErrors (err, req, res, next) { console.error(err.stack) next(err) }
Also in this example, clientErrorHandler is defined as follows; in this case, the error is explicitly passed along to the next one.
Notice that when not calling “next” in an error-handling function, you are responsible for writing (and ending) the response. Otherwise those requests will “hang” and will not be eligible for garbage collection.
function clientErrorHandler (err, req, res, next) { if (req.xhr) { res.status(500).send({ error: 'Something failed!' }) } else { next(err) } }
Implement the “catch-all” errorHandler function as follows (for example):
function errorHandler (err, req, res, next) { res.status(500) res.render('error', { error: err }) }
If you have a route handler with multiple callback functions you can use the route parameter to skip to the next route handler. For example:
app.get('/a_route_behind_paywall', (req, res, next) => { if (!req.user.hasPaid) { // continue handling this request next('route') } else { next() } }, (req, res, next) => { PaidContent.find((err, doc) => { if (err) return next(err) res.json(doc) }) })
In this example, the getPaidContent handler will be skipped but any remaining handlers in app for /a_route_behind_paywall would continue to be executed.
Calls to next() and next(err) indicate that the current handler is complete and in what state. next(err) will skip all remaining handlers in the chain except for those that are set up to handle errors as described above.