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:
| Parameter | What It Is |
|---|---|
req | The request object -- contains data about the incoming HTTP request (URL, headers, body, params) |
res | The response object -- used to send data back to the client |
next | A function that passes control to the next middleware in the stack |
A middleware can do three things:
- Modify
reqorres(add properties, parse data, set headers) - End the request-response cycle (send a response with
res.send(),res.json(), etc.) - 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
| Capability | Example |
|---|---|
| Read request data | Check headers, query params, URL path |
| Modify the request | Add req.user, req.startTime, req.requestId |
| Modify the response | Set custom headers with res.set() |
| End the cycle | Send res.json(), res.status(403).send() |
| Call next | Pass to the next middleware or route handler |
| Pass errors | Call next(err) to jump to error-handling middleware |
| Perform async work | Query 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 Step | Express Equivalent |
|---|---|
| Ticket check | express.json() -- "Do you have a valid request body?" |
| ID verification | Auth middleware -- "Are you who you claim to be?" |
| Security scanner | Validation middleware -- "Is your data safe/correct?" |
| Boarding gate | Route handler -- "Here is your response/flight" |
| Denied at any step | res.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:
| Middleware | Route Handler | |
|---|---|---|
| Purpose | Process, validate, transform | Generate the final response |
Calls next()? | Usually yes | Usually no (sends response instead) |
| Position | Anywhere in the stack | Typically 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
- Middleware = any function with
(req, res, next)that sits in the Express processing pipeline. next()passes control to the next middleware; forgetting it hangs the request.- Order matters -- middleware executes in the order registered with
app.use(). - Middleware can modify
req/res, end the cycle, or pass errors withnext(err). - Route handlers are just middleware that happen to send the final response.
- The standard flow is: global middleware -> route matching -> route middleware -> handler -> response.
Explain-It Challenge
Explain without notes:
- What are the three parameters every middleware receives, and what is each for?
- What happens if a middleware neither calls
next()nor sends a response? - Why does the order of
app.use()calls matter? Give a real example where wrong order causes a bug. - 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 ->