Episode 3 — NodeJS MongoDB Backend Architecture / 3.6 — Middleware in Express

3.6.a — Understanding Middleware

In one sentence: Middleware functions are the building blocks of Express -- they have access to the request object (req), the response object (res), and the next function (next), and they form a sequential pipeline that every HTTP request passes through before a response is sent.

Navigation: <- 3.6 Overview | 3.6.b -- Types of Middleware ->


1. What Is Middleware?

In Express, a middleware is any function that has access to three things:

ParameterWhat It Is
reqThe request object -- contains data about the incoming HTTP request (URL, headers, body, params)
resThe response object -- used to send data back to the client
nextA function that passes control to the next middleware in the stack

A middleware can do three things:

  1. Modify req or res (add properties, parse data, set headers)
  2. End the request-response cycle (send a response with res.send(), res.json(), etc.)
  3. Call next() to pass control to the next middleware
// The simplest middleware
const myMiddleware = (req, res, next) => {
  console.log('A request was made!');
  next(); // Pass control to the next middleware
};

2. The Middleware Signature

Every middleware follows the same signature:

(req, res, next) => {
  // Your logic here
  next(); // or res.send(), res.json(), etc.
}

You can also write it as a named function:

function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();
}

Or as a traditional function expression:

const authenticate = function (req, res, next) {
  if (req.headers.authorization) {
    next();
  } else {
    res.status(401).json({ error: 'Unauthorized' });
  }
};

Key rule: The function must either call next() or send a response. If it does neither, the request hangs indefinitely.


3. The Middleware Pipeline / Chain

Express processes middleware in the order they are registered. Think of it as an assembly line where each station does its job and passes the product to the next station.

Client Request
      |
      v
+---------------------+
| Middleware 1 (log)   |  --> next()
+---------------------+
      |
      v
+---------------------+
| Middleware 2 (auth)  |  --> next()  OR  res.send(401)
+---------------------+
      |
      v
+---------------------+
| Middleware 3 (parse) |  --> next()
+---------------------+
      |
      v
+---------------------+
| Route Handler        |  --> res.json({ data })
+---------------------+
      |
      v
   Response sent
   to client

Each middleware decides: continue (next()) or stop (res.send()).


4. The next() Function

next() is the mechanism that connects the pipeline. When a middleware calls next(), Express moves to the next middleware or route handler in the stack.

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

// Middleware 1
app.use((req, res, next) => {
  console.log('Step 1: Logging');
  next(); // Go to Middleware 2
});

// Middleware 2
app.use((req, res, next) => {
  console.log('Step 2: Authentication check');
  next(); // Go to the route handler
});

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

app.listen(3000);

Console output for GET /:

Step 1: Logging
Step 2: Authentication check
Step 3: Route handler

5. What Happens If You Don't Call next()?

If a middleware does not call next() and does not send a response, the request hangs. The client waits indefinitely until a timeout occurs.

// BAD -- request will hang!
app.use((req, res, next) => {
  console.log('I forgot to call next() or send a response');
  // No next(), no res.send() -- request hangs forever
});

app.get('/', (req, res) => {
  // This never executes
  res.send('This will never be reached');
});

What the client sees: The browser spinner keeps going, or the API client eventually shows a timeout error.

Fix: Always ensure every code path either calls next() or sends a response:

// GOOD -- every path either continues or responds
app.use((req, res, next) => {
  if (req.headers['x-api-key']) {
    next(); // Continue to next middleware
  } else {
    res.status(403).json({ error: 'API key required' }); // End the cycle
  }
});

6. Execution Order Matters

The order in which you register middleware with app.use() determines execution order. First registered = first executed.

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

// This runs FIRST for every request
app.use((req, res, next) => {
  console.log('1. First middleware');
  next();
});

// This runs SECOND
app.use((req, res, next) => {
  console.log('2. Second middleware');
  next();
});

// This runs THIRD (only for GET /)
app.get('/', (req, res) => {
  console.log('3. Route handler');
  res.send('Done');
});

// This middleware is AFTER the route -- it only runs if the route calls next()
app.use((req, res, next) => {
  console.log('4. This only runs if route calls next()');
  next();
});

Common mistake: Placing express.json() after a route that needs the parsed body:

// WRONG ORDER
app.post('/data', (req, res) => {
  console.log(req.body); // undefined -- parser hasn't run yet!
  res.send('Received');
});

app.use(express.json()); // Too late for the route above!
// CORRECT ORDER
app.use(express.json()); // Parse body BEFORE routes

app.post('/data', (req, res) => {
  console.log(req.body); // { name: "Alice" } -- works!
  res.send('Received');
});

7. The Request Lifecycle

Every request follows this lifecycle:

HTTP Request arrives at Express server
         |
         v
  [Global Middleware Stack]
    app.use(express.json())
    app.use(cors())
    app.use(logger)
         |
         v
  [Route Matching]
    Does the URL + method match a route?
         |
    YES  |  NO
     |       |
     v       v
  [Route   [404 handler
  Middleware  or default
  + Handler] Express 404]
     |
     v
  [Response sent]
     |
     v
  [Error middleware -- if next(err) was called]
     (err, req, res, next) => { ... }

8. ASCII Diagram -- Full Middleware Pipeline

REQUEST ──> [ express.json() ] ──> [ cors() ] ──> [ logger() ]
                                                        |
                                                        v
            [ auth middleware ] ──> [ route handler ] ──> RESPONSE
                   |
                   | (if auth fails)
                   v
              403 Forbidden ──> RESPONSE

            [ any unhandled error ]
                   |
                   v
            [ error middleware (err, req, res, next) ]
                   |
                   v
              500 Error ──> RESPONSE

9. What Middleware Can Do

CapabilityExample
Read request dataCheck headers, query params, URL path
Modify the requestAdd req.user, req.startTime, req.requestId
Modify the responseSet custom headers with res.set()
End the cycleSend res.json(), res.status(403).send()
Call nextPass to the next middleware or route handler
Pass errorsCall next(err) to jump to error-handling middleware
Perform async workQuery a database, call an external API, read a file
// Middleware that adds a custom property to req
app.use((req, res, next) => {
  req.requestTime = Date.now();
  req.requestId = Math.random().toString(36).substring(2, 10);
  next();
});

// Route handler can now use those properties
app.get('/status', (req, res) => {
  res.json({
    message: 'OK',
    requestId: req.requestId,
    timestamp: req.requestTime
  });
});

10. Analogy -- Security Checkpoints at an Airport

Think of middleware as airport checkpoints:

Airport StepExpress Equivalent
Ticket checkexpress.json() -- "Do you have a valid request body?"
ID verificationAuth middleware -- "Are you who you claim to be?"
Security scannerValidation middleware -- "Is your data safe/correct?"
Boarding gateRoute handler -- "Here is your response/flight"
Denied at any stepres.status(403).send() -- request rejected, no next()

Just like a traveler must pass each checkpoint in order, a request passes through each middleware. If any checkpoint rejects the traveler, they do not reach the gate.


11. Middleware vs Route Handlers

Route handlers are technically middleware -- they just happen to be the last function in the chain for a matched route.

// These are ALL middleware functions:

// Global middleware (runs for all routes)
app.use(express.json());

// Route-specific middleware + handler
app.get('/users',
  validateQuery,    // middleware 1 for this route
  checkAuth,        // middleware 2 for this route
  (req, res) => {   // the "route handler" -- also middleware
    res.json({ users: [] });
  }
);

The difference is conceptual, not technical:

MiddlewareRoute Handler
PurposeProcess, validate, transformGenerate the final response
Calls next()?Usually yesUsually no (sends response instead)
PositionAnywhere in the stackTypically last for a given route

12. Complete Working Example

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

// --- Global middleware ---

// 1. Parse JSON bodies
app.use(express.json());

// 2. Custom logger
app.use((req, res, next) => {
  const start = Date.now();
  console.log(`--> ${req.method} ${req.url}`);

  // Run after response is sent
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`<-- ${req.method} ${req.url} ${res.statusCode} (${duration}ms)`);
  });

  next();
});

// 3. Add request ID
app.use((req, res, next) => {
  req.id = Math.random().toString(36).slice(2, 10);
  res.setHeader('X-Request-ID', req.id);
  next();
});

// --- Routes ---

app.get('/', (req, res) => {
  res.json({ message: 'Welcome!', requestId: req.id });
});

app.post('/echo', (req, res) => {
  res.json({ received: req.body, requestId: req.id });
});

// --- 404 handler (after all routes) ---
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

// --- Error handler (must be last, 4 params) ---
app.use((err, req, res, next) => {
  console.error('Error:', err.message);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(3000, () => console.log('Server on http://localhost:3000'));

Key Takeaways

  1. Middleware = any function with (req, res, next) that sits in the Express processing pipeline.
  2. next() passes control to the next middleware; forgetting it hangs the request.
  3. Order matters -- middleware executes in the order registered with app.use().
  4. Middleware can modify req/res, end the cycle, or pass errors with next(err).
  5. Route handlers are just middleware that happen to send the final response.
  6. The standard flow is: global middleware -> route matching -> route middleware -> handler -> response.

Explain-It Challenge

Explain without notes:

  1. What are the three parameters every middleware receives, and what is each for?
  2. What happens if a middleware neither calls next() nor sends a response?
  3. Why does the order of app.use() calls matter? Give a real example where wrong order causes a bug.
  4. Draw the middleware pipeline for a POST request that goes through body parsing, authentication, and a route handler.

Navigation: <- 3.6 Overview | 3.6.b -- Types of Middleware ->