Express Middleware Concept
Introduction
In Express.js applications, middleware functions are the workhorses that process incoming HTTP requests before they reach their final route handler. These functions have access to the request object (req
), the response object (res
), and the next middleware function in the application's request-response cycle.
Middleware functions can:
- Execute any code
- Make changes to the request and response objects
- End the request-response cycle
- Call the next middleware function in the stack
Understanding middleware is essential to mastering Express, as it's the fundamental concept that powers the framework's flexibility and extensibility.
What is Middleware?
At its core, middleware is a function with a specific signature:
function middlewareFunction(req, res, next) {
// Middleware logic goes here
next(); // Call next() to pass control to the next middleware
}
The function takes three parameters:
req
: The request objectres
: The response objectnext
: A function to call to pass control to the next middleware
Think of middleware as a series of processing layers that requests flow through before receiving a response:
Client Request → Middleware 1 → Middleware 2 → ... → Route Handler → Response
Middleware Types in Express
Express has several types of middleware:
- Application-level middleware: Bound to the app instance using
app.use()
orapp.METHOD()
- Router-level middleware: Bound to an instance of
express.Router()
- Error-handling middleware: Defined with four arguments
(err, req, res, next)
- Built-in middleware: Express's included middleware like
express.json()
- Third-party middleware: External middleware packages like
morgan
,cors
, etc.
Let's explore each type with examples.
Application-Level Middleware
Application-level middleware is bound to the Express app instance using app.use()
. It runs on every request to the application.
const express = require('express');
const app = express();
// A simple middleware logger
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // Pass control to the next middleware
});
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
When you make a request to this server, the terminal will show:
[2023-10-16T14:25:32.456Z] GET /
Middleware for Specific Paths
You can apply middleware to specific paths:
// This middleware only runs for requests to /users/*
app.use('/users', (req, res, next) => {
console.log('User route accessed');
next();
});
app.get('/users/profile', (req, res) => {
res.send('User Profile');
});
app.get('/about', (req, res) => {
res.send('About Page'); // No middleware log for this route
});
Multiple Middleware Functions
You can apply multiple middleware functions to a single mount point:
const authenticate = (req, res, next) => {
// Authentication logic
console.log('Authenticating...');
req.user = { id: 1, name: 'John' }; // Add user data to request
next();
};
const logActivity = (req, res, next) => {
console.log(`User ${req.user.name} accessed the dashboard`);
next();
};
// Apply both middleware functions to the dashboard route
app.get('/dashboard', authenticate, logActivity, (req, res) => {
res.send(`Welcome to your dashboard, ${req.user.name}!`);
});
Output of accessing /dashboard
:
Authenticating...
User John accessed the dashboard
Router-Level Middleware
Router-level middleware works just like application-level middleware but is bound to an instance of express.Router()
:
const express = require('express');
const router = express.Router();
// Router-level middleware
router.use((req, res, next) => {
console.log('Router-specific middleware');
next();
});
router.get('/products', (req, res) => {
res.send('Products list');
});
// Apply the router to the main app
const app = express();
app.use('/api', router);
// Request to /api/products will go through the router middleware
Error-Handling Middleware
Error-handling middleware has a signature with four parameters instead of three. Express recognizes it as error handling middleware by this signature:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
To use error-handling middleware, you need to call next()
with an error:
app.get('/broken', (req, res, next) => {
try {
// Simulating an error
throw new Error('Something went wrong!');
} catch (err) {
next(err); // Pass error to error-handling middleware
}
});
// Error-handling middleware (must be defined last)
app.use((err, req, res, next) => {
console.error(`Error: ${err.message}`);
res.status(500).send('Server Error');
});
Built-in Middleware
Express includes some built-in middleware functions:
express.json()
Parses incoming requests with JSON payloads:
const express = require('express');
const app = express();
// Parse JSON request bodies
app.use(express.json());
app.post('/api/data', (req, res) => {
console.log(req.body); // Access parsed JSON data
res.json({ received: req.body });
});
express.urlencoded()
Parses incoming requests with URL-encoded payloads:
// Parse URL-encoded form data
app.use(express.urlencoded({ extended: true }));
app.post('/login', (req, res) => {
const { username, password } = req.body;
console.log(`Login attempt: ${username}`);
// Process login...
res.redirect('/dashboard');
});
express.static()
Serves static files like images, CSS, and JavaScript:
// Serve static files from the 'public' directory
app.use(express.static('public'));
// Now files in the 'public' folder are accessible at the root URL
// Example: public/styles.css is available at http://localhost:3000/styles.css
Third-Party Middleware
There are many third-party middleware packages available for Express. Here are a few common ones:
Morgan - HTTP Logger
const express = require('express');
const morgan = require('morgan');
const app = express();
// Use morgan middleware for logging HTTP requests
app.use(morgan('dev'));
app.get('/', (req, res) => {
res.send('Hello World');
});
Output:
GET / 200 5.269 ms - 11
CORS - Cross-Origin Resource Sharing
const express = require('express');
const cors = require('cors');
const app = express();
// Enable CORS for all routes
app.use(cors());
// Or for specific routes/origins
app.use(cors({
origin: 'https://example.com',
methods: ['GET', 'POST']
}));
Creating Custom Middleware
Creating your own middleware is straightforward. Let's build a simple authentication middleware:
// Custom authentication middleware
const authenticate = (req, res, next) => {
const authToken = req.headers.authorization;
if (!authToken || authToken !== 'Bearer secret-token') {
return res.status(401).json({ error: 'Unauthorized' });
}
// Add user information to the request object
req.user = { id: 123, role: 'admin' };
next(); // Continue to the next middleware/route
};
// Apply middleware to specific routes
app.get('/admin', authenticate, (req, res) => {
res.json({
message: 'Admin area',
user: req.user
});
});
Testing this endpoint:
With correct token:
GET /admin
Headers: Authorization: Bearer secret-token
Response:
{
"message": "Admin area",
"user": {
"id": 123,
"role": "admin"
}
}
With incorrect token:
GET /admin
Response:
{
"error": "Unauthorized"
}
Practical Example: A Complete Express Application with Middleware
Let's build a simple Express application that demonstrates multiple middleware types working together:
const express = require('express');
const app = express();
// Middleware 1: Request Logger
app.use((req, res, next) => {
console.log(`Request: ${req.method} ${req.url} at ${new Date().toISOString()}`);
next();
});
// Middleware 2: Parse JSON bodies
app.use(express.json());
// Middleware 3: Add request ID
app.use((req, res, next) => {
req.requestId = Date.now().toString();
next();
});
// Middleware 4: Authenticate specific routes
const authenticate = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== 'valid-api-key') {
return res.status(401).json({ error: 'Invalid API key' });
}
next();
};
// Public route - no authentication needed
app.get('/public', (req, res) => {
res.json({
message: 'Public data',
requestId: req.requestId
});
});
// Protected route - uses authentication middleware
app.get('/protected', authenticate, (req, res) => {
res.json({
message: 'Protected data',
requestId: req.requestId
});
});
// Post endpoint showing JSON body parsing
app.post('/data', (req, res) => {
console.log('Received data:', req.body);
res.json({
received: req.body,
requestId: req.requestId
});
});
// Error-handling middleware
app.use((err, req, res, next) => {
console.error(`Error (${req.requestId}):`, err);
res.status(500).json({
error: 'Server error',
requestId: req.requestId
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
This example demonstrates:
- Global request logging middleware
- Request body parsing with
express.json()
- Adding data to the request object
- Route-specific authentication middleware
- Error handling middleware
Middleware Execution Order
The order in which you define middleware is crucial. Express executes middleware in the order they are defined:
const express = require('express');
const app = express();
// This runs first
app.use((req, res, next) => {
console.log('First middleware');
next();
});
// This runs second
app.use((req, res, next) => {
console.log('Second middleware');
next();
});
app.get('/', (req, res) => {
console.log('Route handler');
res.send('Hello');
});
// This will only run if no other route matches
app.use((req, res) => {
res.status(404).send('Not Found');
});
Output when accessing the root route:
First middleware
Second middleware
Route handler
Advanced Middleware Patterns
Conditional Middleware Execution
You can decide dynamically whether to continue the middleware chain:
// Rate limiting middleware
const simpleRateLimit = (req, res, next) => {
if (Math.random() > 0.8) {
// 20% of requests will be rate limited
return res.status(429).json({ error: 'Too many requests' });
}
next();
};
app.use(simpleRateLimit);
Configurable Middleware
Create middleware that can be configured:
// Configurable logging middleware
const logger = (options = {}) => {
const { showTimestamp = true, showMethod = true } = options;
return (req, res, next) => {
let logMessage = '';
if (showTimestamp) {
logMessage += `[${new Date().toISOString()}] `;
}
if (showMethod) {
logMessage += `${req.method} `;
}
logMessage += req.url;
console.log(logMessage);
next();
};
};
// Use with configuration
app.use(logger({ showTimestamp: true, showMethod: false }));
Summary
Middleware is the backbone of Express applications, providing a powerful way to process requests, modify responses, implement cross-cutting concerns, and build modular applications:
- Middleware functions have access to the request, response objects and the next middleware function
- Middleware can be applied at the application level, router level, or for specific routes
- The order of middleware matters - they run in the sequence they are defined
- Express provides built-in middleware for common tasks
- Custom middleware allows you to implement your own application-specific logic
Understanding middleware is crucial for building effective Express applications as it enables clean separation of concerns and promotes code reusability.
Additional Resources
- Express.js Official Middleware Documentation
- Popular Express Middleware Packages
- Writing Middleware (Express.js Documentation)
Exercises
- Create a middleware that logs the request path, method, and timestamp to a file instead of the console.
- Build a middleware that checks if a user is authenticated using JWT (JSON Web Tokens).
- Create a middleware that measures the response time of each request and logs it.
- Implement a middleware that serves as a simple caching layer for GET requests.
- Create a middleware that sanitizes user input in request bodies to prevent XSS attacks.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)