Back to Blog
Backend ArchitectureOct 26, 2026•6 min read•Express.js

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:

  1. req: The incoming HTTP Request object (headers, body, params, query)
  2. res: The outgoing HTTP Response object (status, json, send, headers)
  3. 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(); 
}
The Golden Middleware Rule: Every middleware function must either:
  • End the request-response cycle (by sending data via res.send(), res.json(), or res.end()), OR
  • Call next() to pass control to the next middleware.
If a middleware does neither, the client request will hang indefinitely until it times out!

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.

Interactive Flow Architecture

The Express.js Middleware Pipeline

Click any stage to trace req, res & next()

3. Custom Auth & Guard Middleware

Type: Auth Guard

Inspects request headers for a Bearer JWT token, validates the signature, and enriches the request by attaching the authenticated user payload to req.user.

Implementation Pattern:
// 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
  }
};
Request / Response Mutation:Enriches req with req.user = { id: 'usr_99', role: 'admin' }
Next Action:Calls next() if valid; calls next(err) if token is invalid

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 ecosystem

Battle-tested community packages added via npm:

corshelmet (security headers)morgan (HTTP logger)cookie-parser

5. Mastering next(): The 3 Invocations

How you invoke next() dictates the entire path of request execution:

CallWhat 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().