Complete Guide to Express Middleware

Complete Guide to Express Middleware

  • March 26, 2022
Table Of Contents

Middleware functions are an integral part of an application built with the Express framework (henceforth referred to as Express application). They access the HTTP request and response objects and can either terminate the HTTP request or forward it for further processing to another middleware function.

Middleware functions are attached to one or more route handlers in an Express application and execute in sequence from the time an HTTP request is received by the application till an HTTP response is sent back to the caller.

This capability of executing the Express middleware functions in a chain allows us to create smaller potentially reusable components based on the single responsibility principle(SRP).

In this article, we will understand the below concepts about Express middleware:

  1. Different types of middleware functions in Express.
  2. Create middleware functions using both JavaScript and TypeScript and attach them to one or more Express routes
  3. Use the middleware functions provided by Express and many third-party libraries in our Express applications.
  4. Use middleware functions as error handlers.

Example Code

This article is accompanied by a working code example on GitHub.

Prerequisites

A basic understanding of Node.js and components of the Express framework is advisable.

Please refer to our earlier article for an introduction to Express.

What is Express Middleware?

Middleware in Express are functions that come into play after the server receives the request and before the response is sent to the client. They are arranged in a chain and are called in sequence.

We can use middleware functions for different types of processing tasks required for fulfilling the request like database querying, making API calls, preparing the response, etc, and finally calling the next middleware function in the chain.

Middleware functions take three arguments: the request object (request), the response object (response), and optionally the next() middleware function:

const express = require('express');
const app = express();
function middlewareFunction(request, response, next){
  ...
  next()
}

app.use(middlewareFunction)

An exception to this rule is error handling middleware which takes an error object as the fourth parameter. We call app.use() to add a middleware function to our Express application.

Under the hood, when we call app.use(), the Express framework adds our middleware function to its internal middleware stack. Express executes middleware in the order they are added, so if we make the calls in this order:

app.use(function1)
app.use(function2)

Express will first execute function1 and then function2.

Middleware functions in Express are of the following types:

  • Application-level middleware which runs for all routes in an app object
  • Router level middleware which runs for all routes in a router object
  • Built-in middleware provided by Express like express.static, express.json, express.urlencoded
  • Error handling middleware for handling errors
  • Third-party middleware maintained by the community

We will see examples of each of these types in the subsequent sections.

Basic Setup for Running the Examples

We need to first set up a Node.js project for running our examples of using middleware functions in Express.

Let us create a folder and initialize a Node.js project under it by running the npm init command:

mkdir storefront
cd storefront
npm init -y

Running these commands will create a Node.js project containing a package.json file.

We will next install the Express framework using the npm install command as shown below:

npm install express --save

When we run this command, it will install the Express framework and also add it as a dependency in our package.json file.

We will now create a file named index.js and open the project folder in our favorite code editor. We are using Visual Studio Code as our source-code editor.

Let us now add the following lines of code to index.js for running a simple HTTP server:

const express = require('express');

const app = express();

// Route for handling get request for path /
app.get('/', (request, response) => {
    response.send('response for GET request');
})

// Route for handling post request for path /products
app.post('/products', (request, response) => {
  ...
  response.json(...)
})

// start the server
app.listen(3000, 
   () => console.log('Server listening on port 3000.'))

In this code snippet, we are importing the express module and then calling the listen() function on the app handle to start our server.

We have also defined two routes which will accept the requests at URLs: / and /products. For an elaborate explanation of routes and handler function, please refer to our earlier article for an introduction to Express.

We can run our application with the node command:

node index.js

This will start a server that will listen for requests in port 3000. We will now add middleware functions to this application in the following sections.

Using Express' Built-in Middleware

Built-in middleware functions are bundled with Express so we do not need to install any additional modules for using them.

Express provides the following Built-in middleware functions:

Function Description
express.static serves static assets
express.json parses JSON payloads
express.urlencoded parses URL-encoded payloads
express.raw parses payloads into a Buffer and makes them available under req.body
express.text parses payloads into a string

Let us see some examples of their use.

Using express.static for Serving Static Assets

We use the express.static built-in middleware function to serve static files such as images, CSS files, and JavaScript files. Here is an example of using express.static to serve our HTML and image files:

const express = require('express');

const app = express();
app.use(express.static('images'))  
app.use(express.static('htmls'))   

app.get('product', (request, response)=>{
  response.sendFile("productsample.html")
})

Here we have defined two static paths named images and htmls to represent two folders of the same name in our root directory. We have also defined multiple static assets directories by calling the express.static() middleware function multiple times.

Our root directory structure looks like this:

.
├── htmls
│   └── productsample.html
├── images
│   └── sample.jpg
├── index.js
├── node_modules

Express looks for the files in the order in which we set the static directories with the express.static middleware function.

In our example, we have defined the images directory before htmls. So Express will look for the file: productsample.html in the images directory first. If the file is not found in the images directory, Express looks for the file in the htmls directory.

Next we have defined a route with url product to serve the static HTML file productsample.html. The HTML file contains an image referred only with the image name sample.jpg:

<html>
<body>
    <h2>My sample product page</h2>
    <img src="sample.jpg" alt="sample"></img>
</body>
</html>

Express looks up the files relative to the static directory, so the name of the static directory is not part of the URL.

Using express.json for Parsing JSON Payloads

We use the express.json built-in middleware function to JSON content received from the incoming requests.

Let us suppose the route with URL /products in our Express application accepts product data from the request object in JSON format. So we will use Express' built-in middleware express.json for parsing the incoming JSON payload and attach it to our router object as shown in this code snippet:

const express = require('express');

const app = express();

// Attach the express.json middleware to route "/products"
app.use('/products', express.json({ limit: 100 }))

// handle post request for path /products
app.post('/products', (request, response) => {
...
...
  response.json(...)
})

Here we are attaching the express.json middleware by calling the use() function on the app object. We have also configured a maximum size of 100 bytes for the JSON request.

We have used a slightly different signature of the use() function than the signature of the function used before. The use() function invoked on the app object here takes the URL of the route: /products to which the middleware function will get attached, as the first parameter. Due to this, this middleware function will be called only for this route.

Now we can extract the fields from the JSON payload sent in the request body as shown in this route definition:

const express = require('express')

const app = express()

// Attach the express.json middleware to route "/products"
app.use('/products', express.json({ limit: 100 }))

// handle post request for path /products
app.post('/products', (request, response) => {
  const products = []

  // sample JSON request
  // {"name":"furniture", "brand":"century", "price":1067.67}

  // JSON payload is parsed to extract 
  // the fields name, brand, and category

  // Extract name of product
  const name = request.body.name                

  // Extract brand of product
  const brand = request.body.brand
  
  // Extract category of product
  const category = request.body.category

  console.log(name + " " + brand + " " + category)
  
...
...
  response.json(...)
})

Here we are extracting the contents of the JSON request by calling request.body.FIELD_NAME before using those fields for adding a new product.

Similarly we can use express' built-in middleware express.urlencoded() to process URL encoded fields submitted through a HTTP form object:

app.use(express.urlencoded({ extended: false }));

Then we can use the same code for extracting the fields as we had used before for extracting the fields from a JSON payload.

Adding a Middleware Function to a Route

Let us now see how to create a middleware function of our own.

As an example, let us check for the presence of JSON content in the HTTP POST request body before allowing any further processing and send back an error response if the request body does not contain JSON content.

Our middleware function for checking for the presence of JSON content looks like this:

const requireJsonContent = (request, response, next) => {
  if (request.headers['content-type'] !== 'application/json') {
      response.status(400).send('Server requires application/json')
  } else {
    next()
  }
}

Here we are checking the value of the content-type header in the request. If the value of the content-type header does not match application/json, we are sending back an error response with status 400 accompanied by an error message thereby ending the request-response cycle.

Otherwise, if the content-type header is application/json, the next() function is invoked to call the subsequent middleware present in the chain.

Next we will add the middleware function: requireJsonContent to our desired route like this:

const express = require('express')

const app = express()

// handle post request for path /products
app.post('/products', requireJsonContent, (request, response) => {
  ...
  ...
  response.json(...)
})

We can also attach more than one middleware function to a route to apply multiple stages of processing.

Our route with multiple middleware functions attached will look like this:

const express = require('express')

const app = express()

// handle post request for path /products
app.post('/products', 
  
  // first function in the chain will check for JSON content
  requireJsonContent,  
  
  // second function will check for valid product category 
  // in the request if the first function detects JSON 
  (request, response) => {  
                           
     // Allow to add only products in the category "Electronics"
     const category = request.body.category
     if(category != "Electronics") {
      response.status(400).send('Server requires application/json')
     } else {
        next()
     }
  ...
  ...
  // add the product and return a response in JSON
  response.json(
    {productID: "12345", 
    result: "success")}
  );

Here we have two middleware functions attached to the route with route path /products.

The first middleware function requireJsonContent() will pass the control to the next function in the chain if the content-type header in the HTTP request contains application/json.

The second middleware function extracts the category field from the JSON request and sends back an error response if the value of the category field is not Electronics.

Otherwise, it calls the next() function to process the request further which adds the product to a database for example, and sends back a response in JSON format to the caller.

We could have also attached our middleware function by using the use() function of the app object as shown below:

const express = require('express')

const app = express()

// first function in the chain will check for JSON content
app.use('/products', requireJsonContent)

// second function will check for valid product category 
// in the request if the first function detects JSON 
app.use('/products',  (request, response) => {  
                           
     // Allow to add only products in the category "Electronics"
     const category = request.body.category
     if(category != "Electronics") {
      response.status(400).send('Server requires application/json')
     } else {
        next()
     }
   })

// handle post request for path /products
app.post('/products', 
  (request, response) => {  
                           
  ...
  ...
  response.json(
    {productID: "12345", 
    result: "success"})
  })

Understanding The next() Function

The next() function is a function in the Express router that, when invoked, executes the next middleware in the middleware stack.

If the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.

When we have multiple middleware functions, we need to ensure that each of our middleware functions either calls the next() function or sends back a response. Express will not throw an error if our middleware does not call the next() function and will simply hang.

The next() function could be named anything, but by convention it is always named “next”.

Adding a Middleware Function to All Requests

We might also want to perform some common processing for all the routes and specify them in one place instead of repeating them for all the route definitions. Examples of common processing are authentication, logging, common validations, etc.

Let us suppose we want to print the HTTP method (get, post, etc.) and the URL of every request sent to the Express application. Our middleware function for printing this information will look like this:

const express = require('express');

const app = express();

const requestLogger = (request, response, next) => {
    console.log(`${request.method} url:: ${request.url}`);
    next()
}

app.use(requestLogger) 

This middleware function: requestLogger accesses the method and url fields from the request object to print the request URL along with the HTTP method to the console.

For applying the middleware function to all routes, we will attach the function to the app object that represents the express() function.

Since we have attached this function to the app object, it will get called for every call to the express application. Now when we visit http://localhost:3000 or any other route in this application, we can see the HTTP method and URL of the incoming request object in the terminal window.

Adding a Middleware Function for Error Handling

Express comes with a default error handler that takes care of any errors that might be encountered in the application. The default error handler is added as a middleware function at the end of the middleware function stack.

We can change this default error handling behavior by adding a custom error handler which is a middleware function that takes an error parameter in addition to the parameters: request, response, and the next() function. The error handling middleware functions are attached after the route definitions.

The basic signature of an error-handling middleware function in Express looks like this:

function customErrorHandler(error, request, response, next) {

  // Error handling middleware functionality

}

When we want to call an error-handling middleware, we pass on the error object by calling the next() function with the error argument like this:

const errorLogger = (error, request, response, next) => {
    console.log( `error ${err.message}`) 
    next(error) // calling next middleware
}

Let us define three middleware error handling functions and add them to our routes. We have also added a new route that will throw an error as shown below:


// Error handling Middleware functions
const errorLogger = (error, request, response, next) => {
  console.log( `error ${error.message}`) 
  next(error) // calling next middleware
}

const errorResponder = (error, request, response, next) => {
  response.header("Content-Type", 'application/json')
    
  const status = error.status || 400
  response.status(status).send(error.message)
}

const invalidPathHandler = (request, response, next) => {
  response.status(400)
  response.send('invalid path')
}
  
app.get('product', (request, response)=>{
  response.sendFile("productsample.html")
})

// handle get request for path /
app.get('/', (request, response) => {
  response.send('response for GET request');
})

app.post('/products', requireJsonContent, (request, response) => {
  ...
})

app.get('/productswitherror', (request, response) => {
  let error = new Error(`processing error in request at ${request.url}`)
  error.statusCode = 400
  throw error
})

app.use(errorLogger)
app.use(errorResponder)
app.use(invalidPathHandler)
app.listen(PORT, () => {
  console.log(`Server listening at http://localhost:${PORT}`)
})

These middleware error handling functions perform different tasks: errorLogger logs the error message,errorResponder sends the error response to the client, and invalidPathHandler responds with a message for invalid path when a non-existing route is requested.

We have next attached these three middleware functions for handling errors to the app object by calling the use() method after the route definitions.

To test how our application handles errors with the help of these error handling functions, let us invoke the route with URL: localhost:3000/productswitherror.

Now instead of the default error handler, the first two error handlers get triggered. The first one logs the error message to the console and the second one sends the error message in the response.

When we request a non-existent route, the third error handler is invoked giving us an error message: invalid path.

Using Third-Party Middleware

We can also use third-party middleware to add functionality built by the community to our Express applications. These are usually available as npm modules which we install by running the npm install command in our terminal window. The following example illustrates installing and loading a third-party middleware named Morgan which is an HTTP request logging middleware for Node.js.

npm install morgan

After installing the module containing the third-party middleware, we need to load the middleware function in our Express application as shown below:

const express = require('express')
const morgan = require('morgan')

const app = express()

app.use(morgan('tiny'))

Here we are loading the middleware function morgan by calling require() and then attaching the function to our routes with the use() method of the app instance.

Developing Express Middleware with TypeScript

TypeScript is an open-source language developed by Microsoft. It is a superset of JavaScript with additional capabilities, most notable being static type definitions making it an excellent tool for a better and safer development experience.

Let us first add support for TypeScript to our Node.js project and then see a snippet of the middleware functions written using the TypeScript language.

Installing TypeScript and other Configurations

For adding TypeScript, we need to perform the following steps:

  1. Install Typescript and ts-node with npm:
npm i -D typescript ts-node
  1. Create a JSON file named tsconfig.json with the below contents in our project’s root folder to specify different options for compiling the TypeScript code as shown here:
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "rootDir": "./",
    "esModuleInterop": true
  }
}
  1. Install the type definitions of the Node APIs and Express to be fetched from the @types namespace by installing the @types/node and @types/express packages as a development dependency:
npm i -D @types/node @types/express

Writing Express Middleware Functions in TypeScript

The Express application is written in TypeScript language in a file named app.ts. Here is a snippet of the code:

import express, { Request, Response, NextFunction } from 'express'
import morgan from 'morgan'

const app = express()
const port: number = 3000

// Define the types to be used in the application
interface Product {
    name: string
    price: number
    brand: string
    category?: string
  }

interface ProductCreationResponse {
    productID: string 
    result: string
} 

// Error object used in error handling middleware function
class AppError extends Error{
    statusCode: number;

    constructor(statusCode: number, message: string) {
      super(message);
  
      Object.setPrototypeOf(this, new.target.prototype);
      this.name = Error.name;
      this.statusCode = statusCode;
      Error.captureStackTrace(this);
    }
}

const requestLogger = (request: Request, response: Response, next: NextFunction) => {
    console.log(`${request.method} url:: ${request.url}`);
    next()
}

app.use(express.static('images'))  
app.use(express.static('htmls'))  
app.use(requestLogger)  

app.use(morgan('tiny'))

app.use('/products', express.json({ limit: 100 }))

// Error handling Middleware functions
const errorLogger = (
      error: Error, 
      request: Request, 
      response: Response, 
      next: NextFunction) => {
        console.log( `error ${error.message}`) 
        next(error) // calling next middleware
  }
  
const errorResponder = (
    error: AppError, 
    request: Request, 
    response: Response, 
    next: NextFunction) => {
        response.header("Content-Type", 'application/json')
          
        const status = error.statusCode || 400
        response.status(status).send(error.message)
  }

const invalidPathHandler = (
  request: Request, 
  response: Response, 
  next: NextFunction) => {
    response.status(400)
    response.send('invalid path')
}
  
  
  
app.get('product', (request: Request, response: Response)=>{
    response.sendFile("productsample.html")
})
  
// handle get request for path /
app.get('/', (request: Request, response: Response) => {
    response.send('response for GET request');
})
  
  
const requireJsonContent = (request: Request, response: Response, next: NextFunction) => {
  if (request.headers['content-type'] !== 'application/json') {
      response.status(400).send('Server requires application/json')
  } else {
    next()
  }
}


const addProducts = (request: Request, response: Response, next: NextFunction) => {
    let products: Product[] = []
...
...
    const productCreationResponse: ProductCreationResponse = {productID: "12345", result: "success"}
    response.json(productCreationResponse)

    response.status(200).json(products);
}

app.post('/products', addProducts)

app.get('/productswitherror', (request: Request, response: Response) => {
    let error: AppError = new AppError(400, `processing error in request at ${request.url}`)
    
    throw error
  })
  
  app.use(errorLogger)
  app.use(errorResponder)
  app.use(invalidPathHandler)

app.listen(port, () => {
    console.log(`Server listening at port ${port}.`)
})

Here we have used the express module to create a server as we have seen before. With this configuration, the server will run on port 3000 and can be accessed with the URL: http://localhost:3000.

We have modified the import statement on the first line to import the TypeScript interfaces that will be used for the request, response, and next parameters inside the Express middleware.

Next, we have defined a type named Product containing attributes: name, price, category, and brand. After we have defined the handler function for returning an array of products and finally associated it with a route with route path /products.

Running the Express Application Written in TypeScript

We run the Express application written in TypeScript code by using the below command:

npx ts-node app.ts

Running this command will start the HTTP server. We have used npx here which is a command-line tool that can execute a package from the npm registry without installing that package.

Conclusion

Here is a list of the major points for a quick reference:

  1. Express middleware refers to a set of functions that execute during the processing of HTTP requests received by an Express application.

  2. Middleware functions access the HTTP request and response objects. They either terminate the HTTP request or forward it for further processing to another middleware function.

  3. We can add middleware functions to all the routes by using the app.use(<middleware function>).

  4. We can add middleware functions to selected routes by using the app.use(<route url>, <middleware function>).

  5. Express comes with built-in middleware functions like:

    • express.static for serving static resources like CSS, images, and HTML files.
    • express.json for parsing JSON payloads received in the request body
    • express.urlencoded for parsing URL encoded payloads received in the request body
  6. Express middleware functions are also written and distributed as npm modules by the community. These can be integrated into our application as third-party middleware functions.

  7. We perform error handling in Express applications by writing middleware functions that handle errors. These error handling functions take the error object as the fourth parameter in addition to the parameters: request, response, and the next function.

  8. Express comes with a default error handler for handling error conditions. This is a default middleware function added by Express at the end of the middleware stack.

  9. We also used TypeScript to define a Node.js server application containing middleware functions,

You can refer to all the source code used in the article on Github.

Written By:

Pratik Das

Written By:

Pratik Das

Software Engineer, Consultant and Architect with current expertise in Enterprise and Cloud Architecture, serverless technologies, Microservices, and Devops.

Recent Posts

Handling Timezones in a Spring Boot Application

It is common to encounter applications that run in different time zones. Handling date and time operations consistently across multiple layers of an application can be tricky.

Read more

JUnit 5 by Examples

In software development, unit testing means writing tests to verify that our code is working as logical units. Different people tend to think of a “logical unit” in different ways.

Read more

Scheduling Jobs with Node.js

Have you ever wanted to perform a specific task on your application server at specific times without physically running them yourself?

Read more