Skip to Content
Lesson 4

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.

  1. The Request (req) is the raw material.
  2. The Response (res) is the finished product, ready to be shipped back to the client.
  3. 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.

server.js
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 with Content-Type: application/json and populates req.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

  1. Create a basic Express server.
  2. 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.
  3. Apply this middleware globally using app.use().
  4. Create two simple routes (/ and /test) that send back a basic text response.
  5. Run the server and make requests to both routes to verify that your logger is working.

Activity 2: Simple Authentication Middleware

  1. Building on the previous activity, create a new middleware function called checkApiKey.
  2. This middleware should check for a special header in the request, x-api-key.
  3. If req.headers['x-api-key'] is equal to 'supersecretkey', it should call next() to allow access.
  4. 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' }).
  5. Apply this middleware only to a new /api route path. Create a public route like / that does not require the key.
  6. Test both the public route and the protected /api route, making sure to add the x-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.


References