Skip to main content

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:

javascript
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 object
  • res: The response object
  • next: 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:

  1. Application-level middleware: Bound to the app instance using app.use() or app.METHOD()
  2. Router-level middleware: Bound to an instance of express.Router()
  3. Error-handling middleware: Defined with four arguments (err, req, res, next)
  4. Built-in middleware: Express's included middleware like express.json()
  5. 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.

javascript
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:

javascript
// 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:

javascript
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():

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

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

javascript
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

javascript
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:

javascript
// 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:

javascript
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:

  1. Global request logging middleware
  2. Request body parsing with express.json()
  3. Adding data to the request object
  4. Route-specific authentication middleware
  5. Error handling middleware

Middleware Execution Order

The order in which you define middleware is crucial. Express executes middleware in the order they are defined:

javascript
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:

javascript
// 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:

javascript
// 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

Exercises

  1. Create a middleware that logs the request path, method, and timestamp to a file instead of the console.
  2. Build a middleware that checks if a user is authenticated using JWT (JSON Web Tokens).
  3. Create a middleware that measures the response time of each request and logs it.
  4. Implement a middleware that serves as a simple caching layer for GET requests.
  5. 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! :)