What is Middleware in Express and How It Works

Let me paint a picture of a backend codebase that is slowly descending into madness.
Imagine you are building a social media API. You have a route to fetch a user's feed, a route to post a photo, and a route to delete a comment.
Before the server actually deletes a comment, you need to do a few things:
Log what time the request came in.
Check if the user is actually logged in.
Check if the user actually wrote the comment they are trying to delete.
If you don't know about middleware, you end up writing those three checks directly inside the deleteComment route. And then you copy and paste those exact same checks into the postPhoto route. And then into the updateProfile route.
Suddenly, your route handlers are hundreds of lines long. If you ever need to change how authentication works, you have to find and replace code in fifty different files.
It's a nightmare.
Express.js solves this elegantly using a concept called Middleware. If you want to master Express, you have to master middleware. It is the absolute beating heart of the framework.
The Checkpoint Analogy
What exactly is middleware?
Think of your Express server like a high-security airport.
When a passenger (an HTTP Request) arrives at the airport, they want to get to their airplane (the Final Route Handler). But they can't just sprint directly from the front door to the cockpit.
They have to pass through a sequence of checkpoints.
The ID Check (Authentication)
The Metal Detector (Validation)
The Baggage Scanner (Body Parsing)
Each checkpoint is an isolated, independent station. The metal detector guy doesn't care about your ID; he just checks for metal. If you pass a checkpoint, the guard points you to the next station. If you fail (maybe you brought water through security), they stop you right there and kick you out of the line (send an Error Response).
In Express, Middleware is simply a function that sits between the incoming request and your final route handler. It catches the request, does something to it, and passes it along.
The Anatomy of a Middleware Function
To write a middleware function, you only need to understand its three core parameters: req, res, and the magical next.
function mySimpleLogger(req, res, next) {
console.log(`Someone visited: ${req.url}`);
next(); // The most important part!
}
req(Request): The data coming in from the user. Middleware can read this, or even attach new data to it.res(Response): The tool to send data back to the user. Middleware can use this to end the request early (e.g.,res.status(401).send("Go away!")).next(): The command to move to the next checkpoint.
The Golden Rule: Don't Forget next()
If you forget to call next() inside your middleware, your request will literally hang forever.
The browser will just spin and spin until it eventually times out. Why? Because you didn't send a response, and you didn't tell Express to move to the next function. You essentially locked the passenger in the metal detector and walked away.
Always, always call next() when your middleware is successfully finished.
The Execution Chain (Order Matters)
Express processes middleware strictly top-to-bottom, exactly in the order you write it.
If you put your "Check User Password" middleware after your "Delete Database Record" route, anyone can delete your database, because the password check happened too late!
Let's look at the three main types of middleware you will use to build this chain.
1. Application-Level Middleware
Application-level middleware runs on every single request that hits your server. You bind it to your app using app.use().
This is perfect for global things like logging or parsing data.
const express = require('express');
const app = express();
// This middleware runs for EVERY route
app.use((req, res, next) => {
console.log(`[\({new Date().toISOString()}] \){req.method} request to ${req.url}`);
next();
});
app.get('/', (req, res) => {
res.send("Welcome Home!");
});
2. Built-in Middleware (The Parsers)
Express actually comes with a few essential middleware functions right out of the box.
When a user submits a form or sends JSON data to your API, Express doesn't automatically know how to read it. It just sees raw text. You have to put a "parser" checkpoint in front of your routes to translate the raw text into a nice, usable JavaScript object.
// This built-in middleware catches incoming JSON, parses it,
// and attaches it to 'req.body' so your routes can read it.
app.use(express.json());
app.post('/users', (req, res) => {
// Without the middleware above, req.body would be undefined!
console.log(req.body.name);
res.send("User created!");
});
3. Router-Level Middleware (The Targetted Approach)
Sometimes, you don't want middleware to run on every request.
You want the public homepage to be visible to everyone, but the /dashboard route should strictly require authentication.
Instead of using app.use(), you can inject middleware directly into specific routes.
// A custom middleware function
function requireLogin(req, res, next) {
if (req.headers.authorization === "VIP-PASS") {
next(); // They have the pass! Let them through.
} else {
res.status(401).send("Access Denied."); // Stop the chain!
}
}
// Route 1: No middleware. Anyone can visit.
app.get('/public', (req, res) => {
res.send("Hello world!");
});
// Route 2: We inject 'requireLogin' into the middle.
// Express runs requireLogin first. If next() is called, it runs the final function.
app.get('/dashboard', requireLogin, (req, res) => {
res.send("Welcome to the secret VIP dashboard.");
});
You can even string multiple middlewares together in an array: app.get('/dashboard', [checkLogin, checkAdmin, checkSubscription], (req, res) => {...}). It keeps your routes incredibly clean and modular.
Real-World Magic: Passing Data Down the Line
Here is a pro-tip that takes your Express skills to the next level.
Because every piece of middleware in the chain shares the exact same req object, they can leave notes for each other.
Imagine a request comes in. Your authentication middleware checks the database, validates the user's token, and figures out exactly who the user is. Instead of making your final route handler fetch the user from the database a second time, the middleware can just attach the user data directly to the req object.
function authenticate(req, res, next) {
const token = req.headers.token;
// Pretend we asked the database who owns this token
const dbUser = { id: 99, name: "Alice", role: "admin" };
// We attach it to the request!
req.currentUser = dbUser;
next();
}
app.delete('/post/:id', authenticate, (req, res) => {
// Our final route doesn't need to touch the database to know who is asking.
// It just reads the note left by the middleware!
if (req.currentUser.role === 'admin') {
res.send("Post deleted!");
}
});
The Takeaway
Without middleware, your Express app would be a giant, tangled web of repetitive logic.
By treating your server like a pipeline—breaking down tasks into small, isolated checkpoints like logging, validating, and authenticating—your code becomes modular and infinitely reusable.
The next time you find yourself writing the exact same if/else check at the top of three different route handlers, stop. Pull that logic out, wrap it in a function with req, res, next, and plug it into the pipeline. Let the assembly line do the work for you.





