Skip to main content

Express.js Middleware with Practical Examples

Middleware is one of those words that sounds way more complicated than it actually is. 

The moment people hear it, they picture some advanced, mysterious concept only "real" developers understand. In reality? It's one of the simplest and most useful ideas in Express.js once you see it in action.

Let's break it down in plain, no-jargon English.

So What Actually Is Middleware?

Picture an onion. Not because it's complicated — because it has layers. Every time a request hits your Express server, it can pass through several "layers" before it finally gets a response. Each one of those layers is a middleware function.

More technically: a middleware function is just a regular function that gets access to the request (what the user is asking for) and the response (what you're about to send back). Inside that function, you can:

  • Run any code you want
  • Change the request or response
  • End the request right there and then
  • Or pass things along to the next layer

That last option — passing things along — is what makes middleware so powerful. It's like a relay race where each runner (middleware function) does their part and then hands off the baton.

The 5 Types of Middleware (Don't Worry, They're Simple)

Before touching code, it helps to know the different "flavors" of middleware you'll run into:

  1. Application-level middleware — Attached directly to your Express app. Can run on every request or just specific routes.
  2. Router-level middleware — Same idea, but scoped to a specific express.Router() instance instead of the whole app.
  3. Error-handling middleware — Special middleware built specifically to catch and deal with errors.
  4. Built-in middleware — Comes free with Express, like express.static for serving files (images, CSS, etc.).
  5. Third-party middleware — Middleware other developers built and published, like body-parser.

Don't stress about memorizing these right now. They'll make much more sense once you see them in action below.

Getting Set Up

Before writing any middleware, make sure Express is installed in your project:

npm install express

Got it installed? Great. Let's write some actual code.

Example 1: Basic Application-Level Middleware

This first example creates middleware that logs a little note every time someone visits your server.

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

// Middleware function
app.use((req, res, next) => {
  console.log(`${req.method} request for '${req.url}'`);
  next(); // Pass control to the next middleware function
});

app.get('/', (req, res) => {
  res.send('Hello, world!');
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

What's happening here, step by step:

  • First two lines: Import Express and fire up your app, just like starting any Express project.
  • app.use(...): This is how you register middleware. Whatever function you put inside app.use() will run on every single request that comes into your server.
  • console.log(...): Every time someone visits your server, this prints out what type of request it was (GET, POST, etc.) and which URL they hit. Super handy for debugging.
  • next(): This is the most important part to understand. Calling next() tells Express, "Okay, I'm finished, let the request continue on to wherever it's supposed to go next." If you forget to call next(), the request just freezes there forever — the user's browser will spin and spin with no response. This is the single most common middleware mistake beginners make.
  • app.get('/', ...): After the middleware does its job and calls next(), this route finally sends back "Hello, world!" to the browser.
  • app.listen(3000, ...): Starts the server on port 3000.

Think of the middleware here as a security guard checking IDs at the door before letting people into the actual party (your route).

Example 2: Router-Level Middleware

Once basic middleware clicks for you, router-level middleware is just a small step further — it's middleware that only applies to a specific group of routes instead of your entire app.

const express = require('express');
const app = express();
const router = express.Router();

// Router-level middleware
router.use((req, res, next) => {
  console.log(`Time: ${Date.now()} - ${req.method} request on router`);
  next();
});

router.get('/about', (req, res) => {
  res.send('About Page');
});

app.use('/router', router);

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Breaking it down:

  • express.Router(): This creates a mini, self-contained version of your app — basically a separate bucket you can drop related routes into.
  • router.use(...): Same idea as app.use(), except this middleware only runs for requests going through this specific router, not your entire application.
  • router.get('/about', ...): A normal route, but it lives inside the router instead of directly on app.
  • app.use('/router', router): This connects, or "mounts," your router onto the app at the /router path. So the /about route above actually becomes accessible at /router/about.

Why does this matter? Because as your app grows, you don't want one giant messy pile of routes and middleware. Router-level middleware lets you group related logic together — like having separate middleware just for your /admin routes versus your /public routes.

Example 3: Error-Handling Middleware

Things go wrong sometimes. A database might fail, a file might not exist, or something unexpected happens. Error-handling middleware is your app's way of catching those problems gracefully instead of crashing.

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

// Application-level middleware causing error
app.get('/error', (req, res, next) => {
  const err = new Error('Something went wrong!');
  err.status = 500;
  next(err); // Pass the error to the error-handling middleware
});

// Error-handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).send(err.message || 'Internal Server Error');
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Here's what's going on:

  • The /error route: This is just a pretend example that deliberately creates an error, so you can see how error-handling works. In a real app, this error might come from something like a failed database query instead.
  • next(err): Notice something different here — instead of calling next() with nothing inside it, we're passing the error object into it. This is a signal to Express: "Hey, skip all the normal middleware, this needs to go straight to the error handler."
  • The error-handling middleware itself: You can always spot error-handling middleware because it takes four arguments instead of the usual three — (err, req, res, next). That extra err parameter is what tells Express "this one handles errors."
  • console.error(err.stack): Logs the full error details to your terminal, which is incredibly useful when you're trying to figure out what broke.
  • res.status(err.status || 500).send(...): Sends a proper error response back to the user, along with the correct status code (or defaults to a generic 500 "Internal Server Error" if none was set).

Think of this like a safety net at a circus. Nobody wants to think about it, but when something falls, it's there to catch it instead of letting the whole show collapse.

Why Should You Actually Care About Middleware?

Because almost everything useful in a real-world Express app happens through middleware:

  • Logging who's visiting your site
  • Checking if a user is logged in before letting them see a page
  • Reading data sent from a form
  • Compressing responses to make your site faster
  • Catching and handling errors so your app doesn't just crash

Once you understand middleware, you basically understand how Express handles everything, because pretty much every feature in Express — including your own routes — works using this same request-response-next() pattern.

Popular posts from this blog

C++ vcpkg Manifest Mode + CMake

 If you've ever tried to install a C++ library and felt like you were assembling furniture without instructions, this article is for you. We're going to talk about vcpkg manifest mode and how it works with CMake , and I'm going to explain it like you're five years old (in a good way — no judgment here). First, Let's Talk About the Problem In most programming languages, adding a library is easy. Python has pip install requests . JavaScript has npm install express . You type one command, and boom, the library shows up in your project. C++ never really had that. For decades, if you wanted to use a library like fmt or nlohmann/json , you had to: Download the source code yourself Figure out how to compile it Tell your compiler where to find the headers Tell your linker where to find the compiled binaries Cry a little vcpkg is Microsoft's answer to this mess. It's a package manager for C++ — like pip or npm , but for C++ libraries. And manifest mode...

How to Set Up a Linux Web Server and Host an HTML Page Easily

Setting up a web server on Linux means spending a fair amount of time in the terminal — Linux leans heavily on the command line rather than clicking through menus, so you'll be typing out instructions more often than not.  If you're new to this, it can feel a little intimidating at first, but the good news is you don't need to become a Linux wizard overnight. A handful of core commands will get you surprisingly far. A few you'll lean on constantly: cd — move between directories ls — see what's in the current directory mkdir — create a new folder nano or vim — edit files right there in the terminal sudo — run something with administrator privileges Get comfortable with these and you'll be able to navigate around, tweak configuration files, and install software without much trouble. You don't need to memorize everything — you just need to be confident enough to follow along with clear instructions, which is exactly what this guide aims to give you....

Linux Network Troubleshooting

If you've spent any time as a sysadmin — or honestly, just as someone who's had to fix their own home network at 11pm — you know that connectivity issues are one of the most common headaches out there. The good news is that a handful of core tools and a methodical approach can take you from "why isn't this working" to a root cause pretty quickly.  This guide walks through the essentials: configuring interfaces, managing routes, and diagnosing problems when things go sideways. Configuring Network Interfaces Your network interfaces are the actual bridge between your machine and the outside world, so getting them configured correctly is step one for any kind of reliable connectivity. Doing It Manually ifconfig is the old-school, tried-and-true tool for this on Unix-like systems. To see everything currently configured, run: ifconfig -a If you need to manually set up a specific interface — assigning an IP, a netmask, and bringing it online — it looks like this: ifconf...