- Express.js Basics
- Express.js HOME
- Express.js Introduction
- Express.js Installation
- Express.js Basic App
- Express.js Routing
- Basics Routing
- Route Parameters
- Handling Query Strings
- Router Middleware
- Middleware
- What is Middleware?
- Application-Level Middleware
- Router-Level Middleware
- Built-In Middleware
- Error-Handling Middleware
- Third-Party Middleware
- Express.js HTTP
- Handling GET Requests
- Handling POST Requests
- Handling PUT Requests
- Handling DELETE Requests
- Templating Engines
- Using Templating Engines
- Setting Up EJS
- Setting Up Handlebars
- Setting Up Pug
- Request/Response
- Request Object
- Response Object
- Handling JSON Data
- Handling Form Data
- Static Files
- Serving Static Files
- Setting Up Static Folders
- Managing Assets
- Express.js Advanced
- Middleware Stack
- CORS in Express.js
- JWT Authentication
- Session Handling
- File Uploads
- Error Handling
- Databases
- Express.js with MongoDB
- MongoDB CRUD Operations
- Express.js with MySQL
- MySQL CRUD Operations
- Deployment
- Deploying Express.js Apps to Heroku
- Deploying Express.js Apps to AWS
- Deploying Express.js Apps to Vercel
Express.js with Middleware Stack
In Express.js, middleware functions are the "glue" that holds your application logic together. Think of middleware as a series of checkpoints or processing stations that a request passes through before a response is sent back to the client. Each station can inspect the request, modify it, perform an action (like logging or authentication), or even block the request entirely.
A middleware stack is simply the collection of these functions arranged in a specific sequence. Mastering how to organize this stack is the difference between a messy, hard-to-debug app and a clean, professional-grade API.
Key Features of Middleware Stack
- Sequential Execution: Express executes middleware in the exact order you define them using
app.use(). If you put an authentication check after a route handler, it will never run for that route. - Request and Response Modification: Middleware can add properties to the
reqobject (likereq.userafter looking up a database) which becomes available to all subsequent functions in the stack. - Error Handling: Express provides a specific pattern for catching errors globally, preventing your server from crashing when something goes wrong.
- Third-Party Middleware: You don't have to reinvent the wheel. You can plug in community-tested packages for security, logging, and data parsing.
- Custom Middleware: You can write your own logic to handle unique business requirements, such as checking if a user has a specific subscription tier.
Components of Middleware Stack
How Middleware Works in Express.js
Every middleware function has access to three arguments: the Request object (req), the Response object (res), and the next function. The next() function is critical; it tells Express to move to the next middleware in the line. If you don't call next() and don't send a response, the request will hang indefinitely.
Example of a basic logging middleware:
const express = require('express');
const app = express();
// Middleware to log request details
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} request to ${req.url}`);
// Crucial: Move to the next function in the stack
next();
});
// Middleware for handling specific route
app.get('/', (req, res) => {
res.send('Hello, world!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
next(), your client (browser/mobile app) will wait until it eventually times out. Always ensure your logic either calls next() or sends a response via res.send() or res.json().
Organizing Middleware in the Stack
Large applications need structure. You can categorize middleware based on where they live in your app:
- Application-Level Middleware: These are bound to the
appobject and run for every single request coming into your server (e.g., CORS headers, logging). - Router-Level Middleware: These are bound to an instance of
express.Router(). They only run for a specific group of routes (e.g., all routes starting with/api/v1). - Error-Handling Middleware: These are "safety net" functions designed to catch any errors thrown by previous middleware.
Example of setting up router-level middleware for an API:
// Global middleware (Application-level)
app.use(express.json()); // Parses incoming JSON data
const apiRouter = express.Router();
// Router-level middleware: Runs only for routes in this router
apiRouter.use((req, res, next) => {
console.log('API Request detected');
next();
});
apiRouter.get('/status', (req, res) => {
res.json({ status: 'API is operational' });
});
// Apply the router to a specific path
app.use('/api', apiRouter);
Error-Handling Middleware
Error-handling middleware is unique because it takes **four** arguments instead of three. Express identifies this by the number of parameters. You should always place these at the very bottom of your middleware stack, after all your routes.
Example:
// Error-handling middleware
app.use((err, req, res, next) => {
console.error('Stack Trace:', err.stack);
// Send a structured error response
res.status(err.status || 500).json({
error: {
message: err.message || 'Internal Server Error',
code: err.code || 'UNKNOWN_ERROR'
}
});
});
req, res, next). If you omit the err parameter, Express will treat it as a regular middleware and it won't catch errors properly.
Third-Party Middleware Integration
The Express ecosystem is huge. Common tasks like security headers, cookie parsing, or logging are best handled by established packages. This keeps your main app.js file clean and maintainable.
Example using morgan (for logging) and helmet (for security):
const morgan = require('morgan');
const helmet = require('helmet');
app.use(helmet()); // Sets various HTTP headers for security
app.use(morgan('combined')); // Standard Apache-style logs
Custom Middleware
Custom middleware is perfect for things like "Request IDs" (useful for tracing logs) or checking if a user is an admin. It keeps your route handlers thin and focused on business logic.
Example of an authentication guard:
const checkAdmin = (req, res, next) => {
const isAdmin = req.get('X-Admin-Key') === 'secret-key';
if (isAdmin) {
next(); // User is admin, proceed
} else {
res.status(403).send('Forbidden: Admin access required');
}
};
// Use it only on sensitive routes
app.get('/admin/dashboard', checkAdmin, (req, res) => {
res.send('Welcome to the admin panel');
});
Chaining Middleware
You aren't limited to one middleware per route. You can pass an array of functions or list them sequentially. This is highly effective for "multi-stage" validation where you check for a token, then check permissions, then validate the request body.
Example:
const validateInput = (req, res, next) => {
if (!req.body.name) return res.status(400).send('Name is required');
next();
};
const checkPermissions = (req, res, next) => {
console.log('Checking permissions...');
next();
};
// Chaining multiple middleware for a single route
app.post('/profile', [validateInput, checkPermissions], (req, res) => {
res.send('Profile updated!');
});
Example of a Practical Middleware Stack
Here is how a real-world Express server might look with a properly ordered stack:
const express = require('express');
const app = express();
// 1. Security & Parsing (Top of the stack)
app.use(express.json());
// 2. Global Logging
app.use((req, res, next) => {
console.log(`Incoming: ${req.method} ${req.path}`);
next();
});
// 3. Custom Logic Middleware
const requireAuth = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'No token provided' });
}
next();
};
// 4. Routes
app.get('/public', (req, res) => {
res.send('This is public data');
});
// Protected route using the middleware
app.get('/private/data', requireAuth, (req, res) => {
res.json({ secret: 'The password is "password123"' });
});
// 5. 404 Handler (Runs if no route matches)
app.use((req, res, next) => {
res.status(404).send('Resource not found');
});
// 6. Error Handler (Bottom of the stack)
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).send('An internal error occurred');
});
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});
Summary
In Express.js, the middleware stack is the engine that drives your application. By understanding that middleware runs sequentially, requires next() to continue, and uses specific signatures for error handling, you can build powerful and modular backends. Always remember to order your middleware from most general (security/parsers) to most specific (route handlers) and finish with a robust error handler.