DEV Community

Cover image for Express Middlewares Explained
Adrian Jiga
Adrian Jiga

Posted on

Express Middlewares Explained

If you’re building with Express.js, you’ve definitely seen the term middleware thrown around. At first, it might seem like an abstract concept, but once you understand it, middleware becomes one of the most powerful parts of Express.

First, make sure to read part 1 here, where I cover the basics.

In this post, I’ll walk you through what middleware is, how to use it effectively, and some common mistakes you’ll want to avoid. By the end, you’ll know exactly how to structure your middleware so your apps stay clean, efficient, and bug-free.


What is Middleware in Express?

Middleware is just a function that runs between the time your server receives a request and when it sends back a response.

Think of it as “in-between” logic. The request comes in → middleware runs → response goes out.

In fact, even a basic Express route handler is technically middleware:

app.get("/users", (req, res) => {
  res.send("Users page")
})
Enter fullscreen mode Exit fullscreen mode

This handler takes in a request (req), a response (res), and has the potential to take in next, which determines whether or not Express should move on to the next piece of middleware. That’s the key idea.


The Anatomy of Middleware

A middleware function typically looks like this:

function myMiddleware(req, res, next) {
  // Do something with req or res
  console.log("Middleware ran")

  // Call next() so the next function in line runs
  next()
}
Enter fullscreen mode Exit fullscreen mode

The three parameters are:

  • req → the incoming request object
  • res → the outgoing response object
  • next → a function that passes control to the next middleware

If you don’t call next(), the chain stops there.


Global Middleware with app.use

If you want middleware to run for every single request, you can use app.use:

function logger(req, res, next) {
  console.log("Request URL:", req.originalUrl)
  next()
}

app.use(logger)
Enter fullscreen mode Exit fullscreen mode

Now, no matter what route you hit, this logger will run first.

Important: Middleware runs in the order it’s defined. If you place your logger after your routes, it won’t run unless you explicitly call next() inside those routes.


Route-Specific Middleware

Sometimes, you only want middleware to apply to a single route. In that case, you can pass middleware functions directly into your route definitions:

function auth(req, res, next) {
  if (req.query.admin === "true") {
    req.admin = true
    next()
  } else {
    res.send("Not authorized")
  }
}

app.get("/users", auth, (req, res) => {
  console.log("User is admin:", req.admin)
  res.send("Users page")
})
Enter fullscreen mode Exit fullscreen mode

Here’s what happens:

  1. The auth middleware runs first.
  2. If the query string includes ?admin=true, it calls next().
  3. Otherwise, it stops the chain and sends back “Not authorized.”

Passing Data Between Middleware

Notice in the example above, we set req.admin = true. Since each middleware shares the same req and res, you can attach custom properties and access them later.

app.get("/users", auth, (req, res) => {
  if (req.admin) {
    res.send("Welcome, Admin!")
  } else {
    res.send("Users page")
  }
})
Enter fullscreen mode Exit fullscreen mode

Common Mistake: Forgetting return

One of the most common bugs with middleware is forgetting that next() doesn’t stop your function, it just calls the next middleware, then comes back and continues running your code.

function auth(req, res, next) {
  if (req.query.admin === "true") {
    req.admin = true
    next()
  }
  res.send("Not authorized") // Still runs!
}
Enter fullscreen mode Exit fullscreen mode

This causes:

Error: Cannot set headers after they are sent to the client
Enter fullscreen mode Exit fullscreen mode

Fix: use return:

function auth(req, res, next) {
  if (req.query.admin === "true") {
    req.admin = true
    return next()
  }
  return res.send("Not authorized")
}
Enter fullscreen mode Exit fullscreen mode

Middleware Runs Before and After next()

Code after next() still runs, but only after the next middleware finishes.

function logger(req, res, next) {
  console.log("Before next()")
  next()
  console.log("After next()")
}

app.get("/users", (req, res) => {
  console.log("Users page")
  res.send("Done")
})
Enter fullscreen mode Exit fullscreen mode

Output:

Before next()
Users page
After next()
Enter fullscreen mode Exit fullscreen mode

This allows you to do things before and after route handlers.


Middleware in a Nutshell

  • Middleware = a function with (req, res, next)
  • Runs in order, top to bottom
  • Call next() to continue the chain
  • Attach data to req or res to share between middleware
  • Always return if you don’t want code to continue after next()
  • Can run before and after the next middleware

Once you internalize this, you’ll see middleware everywhere: logging, authentication, error handling, validation, body parsing, CORS, etc.


Final Thoughts

Middleware is at the heart of Express. It may look like just some functions chained together, but with the right design, you can keep your app organized, reusable, and powerful.

Next time you’re building an Express app, think about what belongs in middleware (e.g. logging, auth, validation, error handling), and your code will thank you.

Follow for practical JavaScript tutorials, web development deep-dives, and those 'aha!' moments that make coding click. New posts weekly!

Top comments (0)