Express.js and Middleware: The High-Level Architecture
Express.js is described as a "fast, unopinionated, minimalist web framework for Node.js." But at its core, an Express application is remarkably simple: it is fundamentally a series of middleware function calls chained together. Here is the high-level architecture of how Express works and how requests flow through its pipeline.
1. What is Express.js, Really?
Node.js includes a native http module. While functional, writing clean APIs with native Node requires manually parsing query strings, buffering raw JSON chunks from TCP streams, handling CORS headers, and writing massive switch(req.url) statements.
Express is a thin, elegant wrapper around Node's native http.createServer(). It introduces two primary superpowers:
1. Express Routing
Maps incoming HTTP verbs (GET, POST, PUT, DELETE) and URL paths to specific controller logic, extracting route parameters like /users/:id.
2. The Middleware Pipeline
A composable chain of functions that intercept, inspect, modify, or terminate HTTP requests before they reach your final route handler.
2. What is Middleware? (The Core Mental Model)
A middleware function is simply a function that has direct access to three things:
- req: The incoming HTTP Request object (headers, body, params, query)
- res: The outgoing HTTP Response object (status, json, send, headers)
- next: A callback function that passes execution to the next middleware in the chain
// The canonical middleware signature:
function myMiddleware(req, res, next) {
// 1. Perform logic / inspect request
console.log(`[${req.method}] ${req.url}`);
// 2. Mutate request or response
req.requestTime = Date.now();
// 3. Hand over control to the next function
next();
}- End the request-response cycle (by sending data via
res.send(),res.json(), orres.end()), OR - Call
next()to pass control to the next middleware.
3. The Middleware Pipeline: How a Request Travels
When an HTTP request enters an Express server, it travels down a linear pipeline (often visualized as an onion or assembly line). Each middleware performs a specific job before handing the baton forward.
The Express.js Middleware Pipeline
3. Custom Auth & Guard Middleware
Inspects request headers for a Bearer JWT token, validates the signature, and enriches the request by attaching the authenticated user payload to req.user.
// 3. Custom authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "Unauthorized" });
try {
req.user = jwt.verify(token, SECRET);
next(); // Pass control forward
} catch (err) {
next(err); // Route error to Error Middleware
}
};4. The 5 Types of Express Middleware
Express categorizes middleware into five distinct types based on how and where they are mounted:
1. Application-Level Middleware
app.use() / app.METHOD()Bound directly to the Express application instance using app.use() or HTTP verbs like app.get(). Executes for every incoming request or for a specific path prefix.
// Executes on EVERY request
app.use((req, res, next) => {
console.log("Global Logger");
next();
});2. Router-Level Middleware
router.use()Works identically to application-level middleware, but is bound to an isolated instance of express.Router(). Ideal for modular routing (e.g. all /api/users routes).
const router = express.Router();
// Protects only routes registered on this router
router.use(authMiddleware);
router.get("/profile", getUserProfile);
app.use("/api/users", router);3. Error-Handling Middleware
(err, req, res, next)Must take exactly 4 parameters so Express recognizes it as an error handler. Whenever any previous middleware calls next(error), Express immediately skips all remaining normal middleware and routes execution directly here.
// Placed at the very bottom of your app file
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message });
});4. Built-in Middleware
express.*Bundled directly with Express without requiring external packages:
express.json(): Parses incoming requests with JSON payloads.express.urlencoded(): Parses URL-encoded form submissions.express.static(): Serves static assets like images, HTML, and CSS files.
5. Third-Party Middleware
npm ecosystemBattle-tested community packages added via npm:
5. Mastering next(): The 3 Invocations
How you invoke next() dictates the entire path of request execution:
| Call | What It Does |
|---|---|
| next() | Standard pass. Immediately advances execution to the next middleware or route handler in line. |
| next(err) | Error signal. Passing any argument (except 'route') instructs Express to abort the normal chain and jump directly to the 4-argument error-handling middleware. |
| next('route') | Bypass current route. Skips the remaining middleware callbacks inside the current route handler and passes control to the next matched route. (Only works in app.METHOD or router.METHOD). |
6. Summary: The Mental Model in 3 Sentences
1. Pipeline Order Matters: Express executes middleware strictly in the order they are defined via app.use().
2. State Lives on req: Middleware communicates downstream by attaching properties to the shared req object (e.g. req.user, req.body).
3. Always End or Forward: Every middleware must either terminate the cycle with a response or invoke next().