Understanding Middleware in Express.js

Summary

Express.js is a backend framework built for Node.js that simplifies building server side applications. This post covers Middleware, the core concept of Express.js.

By Max Rohowsky, Ph.D.

In the world of Express, middleware is the most important concept. Or, as the documentation puts it:

"An Express application is essentially a series of middleware function calls."
Express.js Documentation

But first, what is middleware?

Middleware refers to functions that manage incoming requests from and outgoing responses to the client.

There are two types of middleware in Express: application level and router level.

Below, I'll briefly outline how to send requests to the server. Next, we'll look at each of the middleware types in turn.

Sending Requests

The middleware on the server is triggered when a request is received. If you're developing locally, you could send a GET request by using a curl command like this:

curl -X GET http://localhost:3000/posts

From the frontend, you could use the fetch API which would look like something like this:

// Async function to get posts
const getPosts = async () => {
  try {
    const response = await fetch('/posts')
    const posts = await response.json()
    return posts
  } catch (error) {
    console.error('Error:', error)
  }
}

// Call the function
const posts = await getPosts()

Now, let's look at the two types of middleware.

Application level Middleware

Middleware functions that run on the application level?Also frequently referred to as the global level are bound to an instance of the app object. They can be split into two categories:

  • Method agnostic middleware using app.use(...) runs for every incoming request, regardless of HTTP method. This type of middleware commonly:

    • Modifies requests by parsing JSON bodies, adding headers, or handling logging
    • Can stop the request-response cycle early (for example, if authentication fails)
    • Must call next() to pass control to subsequent middleware function.
  • Method specific middleware like app.get(...) and app.post(...) only runs for requests matching that HTTP method. These functions:

    • Process the request by interacting with databases, transforming data, or executing business logic
    • Usually end the cycle by sending a response with res.send() or res.json()
    • Can optionally call next() to continue the middleware chain

Here's an example that includes both method agnostic and method specific types of middleware:

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

// Middleware that runs for every request
app.use((req, res, next) => {
    console.log('Logging request method:', req.method);
    next();
});

// Middleware that handles requests to '/'
app.get('/', (req, res) => {
    res.send('Welcome to the homepage!');
});

// Middleware that handles requests to '/about'
app.get('/about', (req, res) => {
    res.send('About page');
});

app.listen(3005);

Let's compare what happens when you visit / and /about:

//about
• Middleware is triggered and logs the HTTP method req.method• Middleware is triggered and logs the HTTP method req.method
next() is called, passing control to the next middleware functionnext() is called, passing control to the next middleware function
app.get('/', ...) is executed, and the welcome message is sent back• The middleware for /about is executed, and the response About page is sent back

For the sake of completeness, here's an example of a middleware function for each of the most important HTTP methods:

// GET Request
app.get('/posts', (req, res) =>  res.send('Get all posts'))

// POST Request 
app.post('/posts', (req, res) =>  res.send('Create new post'))

// PUT Request 
app.put('/posts/:id', (req, res) =>  res.send('Update a post'))

// DELETE Request 
app.delete('/posts/:id', (req, res) =>  res.send('Delete a post'))

Router level Middleware

The router-level middleware will only run for routes that are mounted under a specific path. To understand this, consider the example below:

const userRouter = express.Router();

// Mount the router under the specific path '/users'
app.use('/users', userRouter);

// This route will be at /users/profile
userRouter.get('/profile', (req, res) => { 
    res.send('User profile page');
});

Router-level middleware is not global because it's isolated to specific request paths, unlike application-level middleware which can affect all routes.

In the example, the /profile route is mounted under the /users path, so it will be accessible at /users/profile.

Summary

Express.js middleware functions handle requests and responses between client and server. There are two main types:

Application-level middleware:

  • Bound to the app object
  • Can be method-agnostic app.use() for all requests
  • Can be method-specific app.get(), app.post(), etc.
  • Often used for logging, parsing, authentication

Router-level middleware:

  • Bound to express.Router instances
  • Only runs for routes mounted under specific paths
  • Enables modular organization of related routes

Middleware functions can modify request and response objects and must call next() to continue the request flow.

Max RohowskyMax Rohowsky, Ph.D.

Hey, I'm Max

I'm here to see you win, and I hope this post helps along the way - feel free to say "hi":