Introduction
Not long ago, I was working on a project that required building an API in Express.js. Things were going smoothly until I realized I needed a way to log requests, handle authentication, and catch errors without cluttering every single route. That’s when middleware really clicked for me.
If you’re new to Express, middleware might feel like a mysterious black box. But trust me, once you understand it, you’ll see it’s the real backbone of any Express.js app. Let me walk you through how I approached it, and hopefully, you’ll avoid some of the early mistakes I made.
What is Middleware?
Think of middleware as a checkpoint for every request. Each request that hits your server passes through one or more middleware functions before it reaches the final destination (your route handler).
Here’s the simplest example I used when I was experimenting:
function myMiddleware(req, res, next) {
console.log('Middleware running...');
next(); // don’t forget this, or requests will hang forever
}
That next()
function? It’s like telling Express: “I’m done here, pass it to the next handler.” If you forget it (and I definitely did a few times), you’ll stare at your browser forever waiting for a response.
Types of Middleware in Express
When I was first learning, I found it easier to break middleware into categories:
Application-Level Middleware
These run on every request unless you scope them to specific routes.
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
I used this for quick logging in my first project, just to see what was going on.
Router-Level Middleware
Perfect when you want middleware only for a group of routes.
const router = express.Router();
router.use((req, res, next) => {
console.log('Router middleware active');
next();
});
Built-in Middleware
Express saves you some time here with helpers like:
express.json()
– parse JSON bodies
express.urlencoded()
– parse form data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
Third-Party Middleware
This is where the community shines. For my project, I immediately installed morgan
for logging and helmet
for security.
const morgan = require('morgan');
app.use(morgan('dev'));
Error-Handling Middleware
This was a lifesaver. Instead of having try/catch
everywhere, I just dropped this at the end:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
Real-World Use Cases
Here are a few places middleware helped me in real projects:
Authentication – I had an internal dashboard that required a token check:
function authMiddleware(req, res, next) {
if (req.headers['authorization'] === 'secret-token') {
next();
} else {
res.status(401).json({ error: 'Unauthorized' });
}
}
app.use('/dashboard', authMiddleware);
Rate Limiting – Preventing abuse with express-rate-limit
.
Logging Requests – Using morgan
so I didn’t have to manually console.log
.
Serving Static Files – With express.static()
, I served uploaded images without writing extra code.
Best Practices I Learned
- Keep middleware modular – don’t dump everything in
server.js
.
- Always remember
next()
unless you’re sending a response.
- Place error-handling middleware at the end of your middleware stack.
- Don’t reinvent the wheel—use third-party middleware when possible.
- Think of middleware as a pipeline—each one should do a small, focused job.
✅ Conclusion
At first, middleware felt like “extra work,” but once I leaned on it properly, it made my code cleaner, easier to manage, and more professional.
So the next time you’re working on an Express project, don’t treat middleware as an afterthought—it’s what transforms a scrappy app into something maintainable and production-ready.