Skip to main content

Express Route Middleware

Middleware functions are a fundamental part of Express.js applications. They are functions that have access to the request and response objects, and they can execute code, modify request and response objects, end the request-response cycle, or call the next middleware in the stack.

Introduction to Route Middleware

In Express.js, middleware functions are like checkpoints that a request passes through before getting a response. They can perform various tasks such as:

  • Authentication and authorization
  • Logging request details
  • Parsing request bodies
  • Handling errors
  • Modifying the request or response objects

Middleware can be applied at different levels:

  1. Application-level middleware (applied to all routes)
  2. Router-level middleware (applied to specific router instances)
  3. Route-level middleware (applied to specific routes)
  4. Error-handling middleware

In this tutorial, we'll focus on route middleware and how to use it effectively in your Express applications.

Basic Middleware Syntax

A middleware function takes three arguments:

  • req: The request object
  • res: The response object
  • next: A function that passes control to the next middleware

Here's the basic structure:

javascript
function myMiddleware(req, res, next) {
// Do something with req and res

// Call next() to pass control to the next middleware function
next();
}

Creating and Using Route Middleware

Let's create a simple middleware function that logs request information:

javascript
const express = require('express');
const app = express();

// Middleware function
const requestLogger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} to ${req.url}`);
next(); // Don't forget to call next!
};

// Apply middleware to a specific route
app.get('/hello', requestLogger, (req, res) => {
res.send('Hello World!');
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

When a user makes a GET request to /hello, the console will show something like:

[2023-10-14T15:30:45.123Z] GET to /hello

Multiple Middleware Functions

You can apply multiple middleware functions to a route by passing them as additional arguments:

javascript
function validateUser(req, res, next) {
// Check if user is authenticated
if (req.query.userId) {
next(); // User provided, continue to the next middleware
} else {
res.status(401).send('Unauthorized: User ID required');
// Note: we don't call next() here because we're ending the request
}
}

function logUserActivity(req, res, next) {
console.log(`User ${req.query.userId} is accessing the system`);
next();
}

// Apply both middleware functions to a route
app.get('/dashboard', validateUser, logUserActivity, (req, res) => {
res.send('Welcome to your dashboard');
});

Using Router-Level Middleware

When you have multiple routes that need the same middleware, you can apply middleware at the router level:

javascript
const express = require('express');
const router = express.Router();

// Apply middleware to all routes in this router
router.use((req, res, next) => {
console.log('API request received at:', Date.now());
next();
});

// Define routes
router.get('/users', (req, res) => {
res.send('User list');
});

router.get('/products', (req, res) => {
res.send('Product list');
});

// Use the router in the main app
const app = express();
app.use('/api', router);

app.listen(3000);

Now any request to /api/users or /api/products will trigger the logging middleware.

Common Middleware Use Cases

Let's explore some common practical uses for middleware in Express applications.

Authentication Middleware

javascript
function authenticateUser(req, res, next) {
const authToken = req.headers.authorization;

if (!authToken || !authToken.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}

const token = authToken.split(' ')[1];

// In a real app, you would verify the token
// For this example, we'll just check if it's the string "valid-token"
if (token === 'valid-token') {
req.user = { id: 123, name: 'Example User' };
next();
} else {
res.status(401).json({ error: 'Invalid authentication token' });
}
}

app.get('/protected', authenticateUser, (req, res) => {
res.json({
message: 'Protected data',
user: req.user
});
});

Request Validation Middleware

javascript
function validateProductData(req, res, next) {
const { name, price } = req.body;

const errors = [];

if (!name || name.length < 3) {
errors.push('Name must be at least 3 characters long');
}

if (!price || isNaN(price) || price <= 0) {
errors.push('Price must be a positive number');
}

if (errors.length > 0) {
return res.status(400).json({ errors });
}

next();
}

app.post('/products', validateProductData, (req, res) => {
// At this point, we know the data is valid
res.json({ message: 'Product created successfully' });
});

Error Handling Middleware

Error handling middleware has a special signature with four arguments instead of three:

javascript
const express = require('express');
const app = express();

app.get('/divide/:a/:b', (req, res, next) => {
const a = parseInt(req.params.a);
const b = parseInt(req.params.b);

if (b === 0) {
// Create an error and pass it to the next middleware
const error = new Error('Cannot divide by zero');
error.statusCode = 400;
return next(error);
}

res.json({ result: a / b });
});

// Error handling middleware
app.use((err, req, res, next) => {
console.error('Error:', err.message);
res.status(err.statusCode || 500).json({
error: err.message || 'Internal Server Error'
});
});

app.listen(3000);

Creating Reusable Middleware Modules

As your application grows, you'll want to organize middleware into separate files:

javascript
// middleware/logger.js
module.exports = function logger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
};

// middleware/auth.js
module.exports = function auth(req, res, next) {
if (req.session && req.session.user) {
return next();
}
res.redirect('/login');
};

Then import and use them in your main app file:

javascript
const express = require('express');
const logger = require('./middleware/logger');
const auth = require('./middleware/auth');

const app = express();

// Apply logger to all routes
app.use(logger);

// Apply auth to specific routes
app.get('/admin', auth, (req, res) => {
res.send('Admin Dashboard');
});

app.listen(3000);

Third-party Middleware

Express.js has a vast ecosystem of third-party middleware. Here are some popular ones:

  • cors: Enable Cross-Origin Resource Sharing (CORS)
  • morgan: HTTP request logger
  • body-parser: Parse request bodies
  • express-session: Session management
  • helmet: Security headers

Here's how to use them:

javascript
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const bodyParser = require('body-parser');
const session = require('express-session');
const helmet = require('helmet');

const app = express();

// Apply third-party middleware
app.use(helmet()); // Add security headers
app.use(cors()); // Enable CORS for all routes
app.use(morgan('dev')); // Log requests to console
app.use(bodyParser.json()); // Parse JSON request bodies
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
})); // Enable sessions

// Your routes here
app.get('/', (req, res) => {
res.send('Hello World');
});

app.listen(3000);

Middleware Execution Flow

Understanding the order of middleware execution is crucial:

javascript
const express = require('express');
const app = express();

app.use((req, res, next) => {
console.log('Middleware 1 - Start');
next();
console.log('Middleware 1 - End');
});

app.use((req, res, next) => {
console.log('Middleware 2 - Start');
next();
console.log('Middleware 2 - End');
});

app.get('/', (req, res) => {
console.log('Route handler');
res.send('Hello World');
});

app.listen(3000);

When you visit the root route, the console output will be:

Middleware 1 - Start
Middleware 2 - Start
Route handler
Middleware 2 - End
Middleware 1 - End

This demonstrates the "onion" structure of middleware — the request passes through layers on the way in, and the response passes back through them on the way out.

Summary

Route middleware is a powerful feature of Express.js that allows you to:

  1. Process requests before they reach the route handlers
  2. Modify request and response objects
  3. End the request-response cycle early if needed
  4. Pass control to the next middleware function
  5. Handle errors in a centralized way

By mastering middleware, you can build modular, maintainable Express applications that follow the separation of concerns principle.

Additional Resources

Exercises

  1. Create a middleware that tracks the response time of each request and logs it.
  2. Build a rate-limiting middleware that allows only 5 requests per minute from the same IP address.
  3. Write a middleware that checks if a user has the appropriate role to access specific routes.
  4. Create error-handling middleware that formats errors differently based on whether the app is in development or production mode.
  5. Build a middleware chain that validates and sanitizes user input for a form submission.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)