If you’ve ever tried to build an API with Node.js, you’ve probably bumped into the word middleware and wondered what it actually means. The official docs can feel a bit abstract, so in this guide we’ll skip the dry theory and explain what middleware in Express is using plain language and real, copy-paste-ready code snippets.
By the end of this post, you’ll understand how requests travel through your Express app, how to write your own middleware, and how to handle common tasks like logging, authentication, and errors.
What Is Middleware in Express.js?
Middleware in Express is simply a function that sits between the incoming request and the final response. It has access to three things:
- req the request object (what the client sent)
- res the response object (what you’ll send back)
- next a function that passes control to the next middleware in the chain
Think of middleware as a series of checkpoints. Each request passes through them one by one until something sends back a response.
function myMiddleware(req, res, next) {
console.log('A request just came in!');
next(); // pass control to the next middleware
}
That’s it. No magic. Just a function with three parameters.

How the Request-Response Cycle Flows Through Middleware
When a client sends a request to your Express server, it doesn’t jump straight to the route handler. Instead, it walks through a stack of middleware functions in the order you registered them.
Here’s a simplified flow:
- Client sends a request (for example,
GET /users) - Request enters the first middleware
- That middleware does its job, then calls
next() - The request moves to the next middleware
- Eventually it reaches a route handler that sends a response with
res.send()orres.json()
If a middleware doesn’t call next() and doesn’t send a response, the request just hangs. That’s one of the most common beginner bugs.
The 5 Types of Middleware in Express
| Type | Where It Runs | Example Use |
|---|---|---|
| Application-level | Bound to the app instance | Logging, parsing JSON |
| Router-level | Bound to an express.Router() |
Auth for a group of routes |
| Built-in | Shipped with Express | express.json(), express.static() |
| Third-party | Installed via npm | cors, helmet, morgan |
| Error-handling | Has 4 arguments: err, req, res, next | Catching and formatting errors |

Real Code Examples You Can Use Today
1. A Simple Logging Middleware
This logs every request method and URL. Great for debugging.
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
app.get('/', (req, res) => {
res.send('Hello world');
});
app.listen(3000);
Every time a request hits your server, you’ll see a timestamped log line in your terminal.
2. Authentication Check Middleware
Here’s a basic middleware that blocks requests without a valid token.
function requireAuth(req, res, next) {
const token = req.headers['authorization'];
if (!token || token !== 'Bearer my-secret-token') {
return res.status(401).json({ error: 'Unauthorized' });
}
// Token is valid, attach user info and continue
req.user = { id: 42, name: 'Jane' };
next();
}
app.get('/profile', requireAuth, (req, res) => {
res.json({ message: `Welcome ${req.user.name}` });
});
Notice how the middleware either sends a response (blocking the request) or calls next() to let it through. That decision is the whole point of middleware.
3. Built-in JSON Body Parser
Before Express can read JSON sent in a POST request, you need to enable the built-in parser.
app.use(express.json());
app.post('/users', (req, res) => {
console.log(req.body); // now available
res.status(201).json(req.body);
});
4. Router-Level Middleware
You can apply middleware to a specific group of routes instead of the whole app.
const router = express.Router();
router.use((req, res, next) => {
console.log('Admin area accessed');
next();
});
router.get('/dashboard', (req, res) => {
res.send('Admin dashboard');
});
app.use('/admin', router);
5. Error-Handling Middleware
Error handlers are special: they take four arguments instead of three. Express recognizes them by the signature.
app.get('/crash', (req, res) => {
throw new Error('Something broke!');
});
// Error-handling middleware - must have 4 args
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: err.message });
});
Always register error-handling middleware last, after all your routes.
The Golden Rules of Middleware
- Order matters. Middleware runs in the order you call
app.use(). - Always call
next()unless you’re sending a response. - Never call
next()after sending a response or you’ll get a “headers already sent” error. - Keep middleware small and focused. One job per function.
- Error handlers go last and must have four parameters.

Popular Third-Party Middleware Worth Knowing
- morgan production-ready HTTP request logger
- cors enables Cross-Origin Resource Sharing
- helmet sets secure HTTP headers
- express-rate-limit protects against brute-force attacks
- cookie-parser parses cookies from request headers
Install any of them with npm install <name> and plug them in with app.use().
Putting It All Together
Here’s a small but complete Express app showing logging, body parsing, auth, a route, and error handling working together:
const express = require('express');
const app = express();
// 1. Logging
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// 2. Body parser
app.use(express.json());
// 3. Auth middleware (used per-route)
function requireAuth(req, res, next) {
if (req.headers.authorization !== 'Bearer secret') {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
// 4. Routes
app.get('/public', (req, res) => res.send('Open to all'));
app.get('/private', requireAuth, (req, res) => res.send('Top secret'));
// 5. Error handler (always last)
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
app.listen(3000, () => console.log('Server running on :3000'));
Frequently Asked Questions
What is the use of middleware in Express?
Middleware lets you run code between receiving a request and sending a response. It’s used for logging, parsing data, checking authentication, validating input, handling errors, and much more.
What is the difference between middleware and a route handler?
They’re actually similar. A route handler is just the last middleware in the chain that sends the final response. The main difference is that middleware usually calls next() to keep the chain going, while a route handler ends it.
Can I have multiple middleware functions on one route?
Yes. You can chain as many as you want: app.get('/path', mw1, mw2, mw3, handler). They’ll execute in order.
What happens if I forget to call next()?
The request will hang until it times out, because Express doesn’t know whether to move forward or send a response. Always call next() or send a response.
Is middleware only for Express?
No. The middleware pattern exists in many frameworks (Koa, Fastify, ASP.NET, Django), but Express popularized it in the Node.js world. The concept is the same: composable functions running through a pipeline.
How do I write async middleware?
Just declare your function as async and use try/catch to forward errors to next(err), like this:
app.use(async (req, res, next) => {
try {
const data = await fetchSomething();
req.data = data;
next();
} catch (err) {
next(err);
}
});
Wrapping Up
Middleware is the backbone of every Express application. Once you see it as just a function that takes req, res, and next, the mystery disappears. Start small with a logger, add a body parser, build an auth check, and before you know it you’ll be composing clean, maintainable APIs like a pro.
Now open your editor, spin up an Express app, and try writing your own middleware. The best way to learn it is to break it a few times.

