What is Middleware in Express and How It Works
From request to response, middleware controls everything. Once you understand it, Express becomes powerful, predictable, and scalable.

Hi, I’m Abdul Samad. A web development learner and tech enthusiast. I write about what I learn, share practical coding tips, and publish in-depth blogs on programming and modern web development.
Check out my full collection of blogs on Hashnode: https://abdulsamad30.hashnode.dev/
Connect with me on X for quick updates and insights: @abdul_sama60108
When you first start building APIs with Express.js, you'll hear the word "middleware" everywhere. It sounds technical, maybe even intimidating. But middleware is actually one of the simplest and most powerful concepts in Express and once it clicks, you'll wonder how you ever built anything without it.
In this post, we'll walk through exactly what middleware is, where it fits in the request lifecycle, the different types you'll encounter, and how to use it in real-world scenarios like logging, authentication, and validation.
What Middleware Is in Express
At its core, middleware is a function that sits between the incoming request and the outgoing response. It has access to the request object (req), the response object (res), and a special function called next() that tells Express to move on to the next step in the chain.
Think of middleware as a series of checkpoints that every request must pass through before reaching its final destination the route handler. At each checkpoint, the middleware can inspect the request, modify it, do something useful, or even stop the request entirely if something isn't right.
An airport is a good way to think about it. When you arrive for a flight, you don't go straight to the plane. You pass through check-in, then security screening, then passport control, then boarding. Each step does something specific verifies your ticket, checks your bags, confirms your identity. Middleware works the same way. Every request passes through a series of functions before reaching the route that sends back a response.
const express = require('express');
const app = express();
// A simple middleware function
function greetMiddleware(req, res, next) {
console.log('Middleware: Hello! A request just came in.');
next(); // Pass control to the next function in the chain
}
app.use(greetMiddleware);
app.get('/', function (req, res) {
res.send('Hello from the route handler!');
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (when a GET request is made to /):
Server running on port 3000
Middleware: Hello! A request just came in.
Response sent to client:
Hello from the route handler!
The middleware ran first, logged a message, called next(), and Express moved on to the route handler. Without next(), the request would have stopped right there at the middleware the route handler would never run.
Where Middleware Sits in the Request Lifecycle
Every time a request hits your Express server, it enters a defined path the request lifecycle. Middleware functions are positioned along this path, and they execute in the order they were registered. The request flows through each middleware, one at a time, until it reaches the final route handler that sends the response.
The lifecycle looks like this:
A client sends an HTTP request to your server
Express receives the request
The request passes through each registered middleware in order
Each middleware can inspect, modify, or respond to the request
If a middleware calls
next(), the request moves to the next middlewareEventually, a route handler (or a middleware) sends the response back
The response travels back to the client
The key insight is that middleware is part of the request path, not separate from it. Every request your server receives will pass through the middleware you've registered in the order you registered it.
const express = require('express');
const app = express();
// Middleware 1 logs the request
app.use(function (req, res, next) {
console.log('Step 1: Request received at', req.url);
next();
});
// Middleware 2 adds a timestamp
app.use(function (req, res, next) {
req.requestTime = new Date().toISOString();
console.log('Step 2: Timestamp added');
next();
});
// Route handler sends the response
app.get('/', function (req, res) {
console.log('Step 3: Route handler reached');
res.json({
message: 'Request processed successfully',
time: req.requestTime
});
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (terminal):
Server running on port 3000
Step 1: Request received at /
Step 2: Timestamp added
Step 3: Route handler reached
Response sent to client:
{
"message": "Request processed successfully",
"time": "2024-01-15T10:30:00.000Z"
}
The Role of the next() Function
The next() function is the mechanism that keeps the middleware chain moving. Without it, Express doesn't know that you want to pass control to the next middleware or route handler. The request simply stops.
This is important to internalize: if you don't call next(), the chain ends right there. This can be intentional like when a middleware sends an error response and you want to stop the request from going further. But if you accidentally forget next() in a middleware that should pass control forward, your request will hang and the client will wait forever.
const express = require('express');
const app = express();
// Middleware WITH next() passes control forward
function passThrough(req, res, next) {
console.log('Middleware: I pass control forward');
next();
}
// Middleware WITHOUT next() stops the chain
function stopHere(req, res, next) {
console.log('Middleware: I stop the chain right here');
res.send('Request stopped at middleware route never reached');
// next() is NOT called the chain ends
}
app.use('/pass', passThrough);
app.use('/stop', stopHere);
app.get('/pass', function (req, res) {
res.send('Route handler reached! The chain worked.');
});
app.get('/stop', function (req, res) {
res.send('You will never see this message.');
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (GET /pass):
Middleware: I pass control forward
Response: Route handler reached! The chain worked.
Expected Output (GET /stop):
Middleware: I stop the chain right here
Response: Request stopped at middleware route never reached
Notice that for /stop, the route handler's response was never reached. The middleware intercepted the request and ended the chain by sending a response without calling next(). This is exactly how authentication middleware works if the user isn't verified, send an error and stop everything.
Types of Middleware
Express supports several types of middleware. Understanding the differences helps you organize your code and apply the right middleware in the right place.
Application-Level Middleware
Application-level middleware is bound to the entire Express app using app.use() or app.METHOD(). It runs for every request that matches the specified path (or all requests if no path is given).
This is the most common type the middleware you'll write and use the most.
const express = require('express');
const app = express();
// Application-level middleware runs on ALL requests
app.use(function (req, res, next) {
console.log(`[\({new Date().toISOString()}] \){req.method} ${req.url}`);
next();
});
// Application-level middleware for a specific path
app.use('/admin', function (req, res, next) {
console.log('Admin area accessed');
next();
});
app.get('/', function (req, res) {
res.send('Home page');
});
app.get('/admin', function (req, res) {
res.send('Admin dashboard');
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (GET /):
[2024-01-15T10:30:00.000Z] GET /
Expected Output (GET /admin):
[2024-01-15T10:30:00.000Z] GET /admin
Admin area accessed
The first middleware logs every request. The second middleware only runs for requests to /admin. Path-specific middleware lets you apply logic only where it's needed.
Router-Level Middleware
Router-level middleware works the same way as application-level middleware, but it's bound to an Express Router instance instead of the app. This is useful when you want to organize middleware specifically for a group of related routes.
const express = require('express');
const app = express();
const router = express.Router();
// Router-level middleware only runs for routes on this router
router.use(function (req, res, next) {
console.log('Router middleware: Processing user route');
next();
});
router.get('/', function (req, res) {
res.json({ message: 'All users' });
});
router.get('/:id', function (req, res) {
res.json({ message: `User ${req.params.id}` });
});
// Mount the router on a path
app.use('/users', router);
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (GET /users):
Router middleware: Processing user route
Response: { "message": "All users" }
Expected Output (GET /users/5):
Router middleware: Processing user route
Response: { "message": "User 5" }
The router middleware only runs for routes defined on that router. Requests to other paths on the app never touch it. This is clean, modular organization.
Built-In Middleware
Express comes with a few middleware functions built right in you don't need to write them yourself.
The most commonly used built-in middleware is express.json(), which parses incoming request bodies that contain JSON. Without it, req.body would be undefined when a client sends JSON data.
const express = require('express');
const app = express();
// Built-in middleware parses JSON request bodies
app.use(express.json());
// Built-in middleware serves static files from a folder
app.use(express.static('public'));
app.post('/data', function (req, res) {
console.log('Parsed body:', req.body);
res.json({
message: 'Data received',
receivedData: req.body
});
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (POST /data with body: {"name":"Alice","age":28}):
Parsed body: { name: 'Alice', age: 28 }
Response:
{
"message": "Data received",
"receivedData": { "name": "Alice", "age": 28 }
}
Without express.json(), req.body would be undefined. This built-in middleware handles the parsing automatically so you can work with structured data in your routes.
Execution Order of Middleware
The order in which you register middleware matters a lot. Express executes middleware in the exact order it's registered. If middleware A is registered before middleware B, A runs first. Every time. No exceptions.
This is a feature, not a limitation. It gives you precise control over the request pipeline. You just need to be intentional about the order.
const express = require('express');
const app = express();
// First runs first
app.use(function (req, res, next) {
console.log('1: First middleware');
next();
});
// Second runs second
app.use(function (req, res, next) {
console.log('2: Second middleware');
next();
});
// Third runs third
app.use(function (req, res, next) {
console.log('3: Third middleware');
next();
});
app.get('/', function (req, res) {
console.log('4: Route handler');
res.send('All middleware passed through in order!');
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output:
Server running on port 3000
1: First middleware
2: Second middleware
3: Third middleware
4: Route handler
Clean, predictable, top to bottom. This is exactly why ordering matters if your authentication middleware is registered after your route handlers, the routes will execute before authentication is ever checked. Always put what should run first at the top.
Real-World Middleware Examples
Theory is useful, but real-world examples are what make middleware click. Here are three of the most common middleware use cases you'll encounter in production applications.
Logging Middleware
Every API needs logging. You want to know what requests are coming in, when, and from where. Logging middleware does this automatically for every request without cluttering your route handlers.
const express = require('express');
const app = express();
// Logging middleware
function logger(req, res, next) {
const timestamp = new Date().toISOString();
console.log(`[\({timestamp}] \){req.method} \({req.url} from \){req.ip}`);
next();
}
app.use(logger);
app.get('/', function (req, res) {
res.send('Home page');
});
app.get('/about', function (req, res) {
res.send('About page');
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (when both routes are accessed):
Server running on port 3000
[2024-01-15T10:30:00.000Z] GET / from ::1
[2024-01-15T10:30:05.000Z] GET /about from ::1
One middleware function, registered once, logs every single request that hits your server. No repetition, no clutter in route handlers.
Authentication Middleware
Authentication middleware checks whether a request is authorized before letting it through. If the user isn't authenticated, you send back an error and stop the chain no next(), no route handler.
const express = require('express');
const app = express();
app.use(express.json());
// Authentication middleware
function authenticate(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader || authHeader !== 'Bearer my-secret-token') {
res.status(401).json({ error: 'Unauthorized valid token required' });
return; // Stops the chain next() is NOT called
}
console.log('Authentication passed');
next(); // Token is valid let the request through
}
// Public route no auth needed
app.get('/public', function (req, res) {
res.json({ message: 'This is public anyone can access it' });
});
// Protected route auth middleware applied
app.get('/profile', authenticate, function (req, res) {
res.json({ message: 'Welcome to your profile!', user: 'authenticated_user' });
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (GET /public no token):
{ "message": "This is public anyone can access it" }
Expected Output (GET /profile no token):
{ "error": "Unauthorized valid token required" }
Status: 401
Expected Output (GET /profile with header Authorization: Bearer my-secret-token):
Authentication passed
{ "message": "Welcome to your profile!", user: "authenticated_user" }
The pattern is clear: check the credentials, and either call next() to let the request proceed or send an error response to stop it. This is exactly how real authentication systems work.
Request Validation Middleware
Validation middleware checks whether the incoming data meets your requirements before your route handler processes it. This keeps your route handlers clean and focused on business logic instead of input checking.
const express = require('express');
const app = express();
app.use(express.json());
// Validation middleware for user creation
function validateUser(req, res, next) {
const { name, email } = req.body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
res.status(400).json({ error: 'Valid name is required' });
return;
}
if (!email || !email.includes('@')) {
res.status(400).json({ error: 'Valid email is required' });
return;
}
console.log('Validation passed for user:', name);
next();
}
app.post('/users', validateUser, function (req, res) {
const { name, email } = req.body;
res.status(201).json({
message: 'User created successfully',
user: { name, email }
});
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (POST /users with body: {"name":"Alice","email":"alice@example.com"}):
Validation passed for user: Alice
Response (Status 201):
{
"message": "User created successfully",
"user": { "name": "Alice", "email": "alice@example.com" }
}
Expected Output (POST /users with body: {"name":"","email":"invalid"}):
{ "error": "Valid name is required" }
Status: 400
The validation logic lives entirely in the middleware. The route handler assumes that by the time it runs, the data is valid. This separation makes both the middleware and the route handler easier to understand and maintain.
Wrapping Up
Middleware is the backbone of Express.js applications. It's how you add functionality, enforce rules, and organize logic across your entire API without repeating code in every route handler. Let's bring everything together:
Middleware is a function that sits between the incoming request and the outgoing response, with access to
req,res, andnext().Execution order is determined by registration order what you register first runs first.
next()is the control flow mechanism call it to pass control forward, omit it to stop the chain.Application-level middleware runs for all requests (or a specific path) on the app.
Router-level middleware runs only for routes on a specific router instance.
Built-in middleware like
express.json()handles common tasks automatically.Real-world use cases logging, authentication, and validation are where middleware genuinely shines.
The pattern is consistent across all these use cases: inspect the request, do something useful, and either call next() or send a response. Master that pattern and you've mastered middleware.
FIN ✌️




