Express Middleware
Workplace Context
Your company’s web application is growing, and so are the demands for security, performance, and monitoring. Before a request to fetch user data hits the main logic, you need to check if the user is properly authenticated. For every request that comes in, you need to log its details for debugging purposes. And for POST
requests, you need to ensure the incoming data is in the correct format. These “cross-cutting concerns” are handled efficiently and elegantly in Express using middleware.
Learning Objectives
By the end of this lesson, you will be able to:
- Explain the concept of middleware using an analogy.
- Understand the role and importance of the
next()
function. - Implement built-in Express middleware like
express.json()
. - Create your own custom middleware for tasks like logging and authentication.
- Understand how the order of middleware affects the request-response cycle.
What is Middleware?
Middleware functions are functions that sit in the middle of the request-response cycle. They have access to the request object (req
), the response object (res
), and a special function called next
.
Analogy: The Factory Assembly Line
Think of a request coming into your Express server as a product moving down a factory assembly line.
- The Request (
req
) is the raw material. - The Response (
res
) is the finished product, ready to be shipped back to the client. - Middleware functions are the workers at various stations along the assembly line.
Each worker (middleware) can do one of three things:
- Inspect and Modify the Product: A worker can inspect the
req
object, add information to it (like attaching user data), or change it. - Reject the Product: If there’s a problem (e.g., bad data, failed authentication), a worker can pull the product off the line and send back an error response (
res.send()
). The cycle ends. - Pass the Product to the Next Station: If everything is okay, the worker calls
next()
, passing the product along to the next worker on the line.
If a worker fails to call next()
or send a response, the assembly line grinds to a halt. The product gets stuck, and the client’s request will eventually time out.
The next()
function
The next()
function is the most critical part of this pattern. It’s the signal that a middleware has finished its job and that the next function in the chain should take over.
const myLoggerMiddleware = (req, res, next) => {
// 1. Do something, like log the request path
console.log('Request received for path:', req.path);
// 2. We are done here. Pass control to the next middleware.
next();
};
If you forget to call next()
, the request stops right here. No other middleware or route handlers after this one will ever run.
Types of Middleware
1. Application-level Middleware (Global)
This is the most common type. You bind it to the app
object using app.use()
. It runs for every single request that comes into your application, regardless of the path or HTTP method. This is perfect for logging, parsing bodies, or handling authentication that applies everywhere.
const express = require('express');
const app = express();
const port = 3000;
// This is a global middleware. It will run for EVERY request.
app.use((req, res, next) => {
req.requestTime = Date.now(); // Add a new property to the request object
console.log('Middleware 1: Time set');
next();
});
// This is another global middleware.
app.use((req, res, next) => {
console.log('Middleware 2: Just checking in!');
next();
});
app.get('/', (req, res) => {
console.log('Route Handler: Now handling the request.');
res.send(`Request received at timestamp: ${req.requestTime}`);
});
app.listen(port, () => {
console.log(`Server is running!`);
});
2. Router-level Middleware (Specific to a Path)
Sometimes, you only want middleware to run for a specific group of routes. You can do this by providing a path to app.use()
.
// This middleware will ONLY run for requests whose path starts with '/admin'
app.use('/admin', (req, res, next) => {
if (req.query.user === 'admin') {
console.log('Admin access granted.');
next();
} else {
res.status(403).send('Forbidden: You must be an admin.');
}
});
// This route is protected by the middleware above
app.get('/admin/dashboard', (req, res) => {
res.send('Welcome to the Admin Dashboard!');
});
// This route is NOT protected
app.get('/public', (req, res) => {
res.send('This is a public page.');
});
3. Built-in Middleware
Express provides several powerful middleware functions out of the box.
express.json()
: We’ve seen this one! It parses incoming requests withContent-Type: application/json
and populatesreq.body
.express.urlencoded({ extended: false })
: Parses requests where the data is URL-encoded (like from a standard HTML form submission).express.static('public')
: Serves static files (HTML, CSS, images) from a given directory.
Middleware Order Matters!
Middleware functions are executed in the exact order they are added to the code. This is extremely important.
Wrong Order:
// 1. Route handler
app.post('/users', (req, res) => {
// This will fail! req.body will be undefined because the
// JSON parser hasn't run yet.
res.send(`User created: ${req.body.name}`);
});
// 2. Middleware
// This will never be reached for the route above.
app.use(express.json());
Correct Order:
// 1. Middleware
// This MUST come before any routes that need to read the request body.
app.use(express.json());
// 2. Route handler
app.post('/users', (req, res) => {
// This now works perfectly!
res.send(`User created: ${req.body.name}`);
});
Activities
Activity 1: Request Logger
- Create a basic Express server.
- Write a custom middleware function that logs the HTTP method (
req.method
), the URL path (req.path
), and the timestamp of every incoming request to the console. - Apply this middleware globally using
app.use()
. - Create two simple routes (
/
and/test
) that send back a basic text response. - Run the server and make requests to both routes to verify that your logger is working.
Activity 2: Simple Authentication Middleware
- Building on the previous activity, create a new middleware function called
checkApiKey
. - This middleware should check for a special header in the request,
x-api-key
. - If
req.headers['x-api-key']
is equal to'supersecretkey'
, it should callnext()
to allow access. - If the key is missing or incorrect, it should end the request by sending a
401 Unauthorized
status and a JSON response:res.status(401).json({ error: 'Unauthorized: API Key required' })
. - Apply this middleware only to a new
/api
route path. Create a public route like/
that does not require the key. - Test both the public route and the protected
/api
route, making sure to add thex-api-key
header for the protected one.
Knowledge Check
What is the role of the next()
function in Express middleware?
- Select an answer to view feedback.
If you want the express.json()
middleware to run for a specific POST route, where must it be placed?
- Select an answer to view feedback.
What happens if a middleware function neither sends a response nor calls next()
?
- Select an answer to view feedback.
Summary
In this lesson, you learned about middleware, one of the most fundamental concepts in Express. You now understand that middleware functions are the building blocks of an Express application, processing requests in a chain like stations on an assembly line. You practiced using built-in middleware and creating your own custom middleware for logging and authentication. This skill is crucial for building clean, modular, and secure web applications.