Max Rohowsky, Ph.D.

Understanding Middleware in Express.js

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? As the name suggests, it's a thing that sits in the middle of something. Specifically, it sits between incoming requests from and outgoing responses to the client.

To manage incoming requests on the server, Express documentation offers two types of middleware: application level and router level.

To begin, 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 that runs on the application level 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 or route handlers
  • Method specific middleware like app.get(...) and app.post(...) only runs for requests matching that HTTP method. These handlers:

    • 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();
});

// Route handler for '/'
app.get('/', (req, res) => {
    res.send('Welcome to the homepage!');
});

// Route handler for '/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 or route handlernext() is called, passing control to the next middleware or route handler
app.get('/', ...) is executed, and the welcome message is sent back• The route handler for /about is executed, and the response About page is sent back

For the sake of completeness, here's an example 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');
});

The /profile route will only be mounted under the /users path, so it will be accessible at /users/profile.

To summarize: Application level middleware applies globally, while router level middleware applies relative to its mounted route(s).

Max Rohowsky

Hey, I'm Max

I'm a Finance Ph.D. and Engineer, and I write about software development and indie hacking.