Express has one real architectural idea, and almost everything else about the framework follows from it: a request and a response travel through an ordered chain of small functions, each of which can inspect them, change them, or end the exchange.
That's the entire model.
There's no plugin tree, no dependency injection container, no schema compiler - just a pipeline, and a deliberate bet that most server-side problems can be decomposed into small, composable steps applied to shared state.
This page is the mental model behind that bet: where the pipeline came from, how dispatch actually works, and what Express intentionally left out so other frameworks - and this section's other pages - had to fill in.
Express Basics walks through building routes and middleware hands-on; Middleware Ordering covers the practical sequencing rules that follow directly from the model described here.
Express dispatches every request through one ordered chain of (req, res, next) functions, all operating on the same shared request and response objects.
Insight: Nearly every Express-specific bug - a missing body, a double response, an error handler that never fires - is really a misunderstanding of this one chain, not a framework quirk.
Key Concepts:middleware, the dispatch chain, next(), Router, prototype extension.
When to Use This Model: Reasoning about why middleware order matters, deciding where cross-cutting logic belongs, and understanding what a Router does and doesn't isolate.
Limitations/Trade-offs: The shared-state, linear-chain design has no built-in encapsulation and a real performance ceiling under very high request rates - both direct results of prioritizing simplicity over structure.
Related Topics: the raw HTTP request/response model, Fastify's plugin encapsulation, NestJS's DI-driven pipeline, error-handling middleware.
Express traces directly back to Connect, an earlier Node middleware library built on the same idea: a list of functions, each given a chance to act on a request before passing control along.
Express kept that core and added routing (app.get, app.post, path parameters) on top of it, but the underlying execution model - a chain, not a tree or a graph - never changed.
An Express app is, at its core, a function you hand to http.createServer() as the request listener; see The Node.js HTTP Server Model for what that listener actually receives from Node.
When a request arrives, Express takes the raw IncomingMessage/ServerResponse pair and extends them with extra methods and properties (req.params, res.json(), and so on) by attaching Express's own prototype - the objects your handlers see are the same Node objects, just with more capability layered on.
Middleware is simply any function matching the shape (req, res, next) => void - it can read or mutate the shared req/res, and it decides whether the chain continues by calling next() or ends by sending a response.
function requestId(req: Request, _res: Response, next: NextFunction) { req.headers["x-request-id"] ??= crypto.randomUUID(); next(); // hand control to the next function in the chain}
A useful analogy: picture a single assembly line, not a branching factory floor - each station (middleware) either stamps something onto the part passing through, or pulls it off the line entirely and ships it (sends the response).
Internally, app.use() and app.get()/app.post() don't build a routing tree - they append layers to an ordered list, each layer wrapping a path/method matcher plus the handler function.
Dispatch walks that list from the top: for each incoming request, Express checks whether the next layer's path and method match, runs it if so, and waits for that layer to call next() before moving to the following one.
This is exactly the linear-chain shape shown conceptually in Middleware Pattern - Express's real dispatcher is a more optimized version of that same idea, not a different algorithm.
A Router is a mountable sub-chain, not an isolated scope: app.use('/users', usersRouter) inserts the router's own internal layer list at that path prefix, but any prototype extension, global middleware side effect, or shared object mutation still applies everywhere - there's no encapsulation boundary the way there is in Fastify's plugin model.
next(err) is the chain's only branching mechanism: calling next() with an argument skips every remaining normal layer and jumps straight to the nearest four-argument error-handling middleware, which is why error handlers must be registered last - they're the fallback destination for that jump, not a special hook Express calls automatically.
Express 5 changed one important piece of this mechanic: rejected Promises returned from an async handler are now automatically forwarded into that same next(err) path, where Express 4 silently swallowed them unless you added express-async-errors or wrapped every handler by hand.
The pipeline model's biggest strength is also its biggest constraint: because every middleware shares the same req/res and runs strictly in registration order, reasoning about a large Express app is really reasoning about one long, flat list - there's no module boundary forcing you to think in smaller pieces the way Fastify's plugin tree or NestJS's module graph do.
That flatness scales fine for most CRUD APIs, but it has a real performance ceiling: each layer in a long chain adds a function call and a path-match check to every request, even ones a given middleware doesn't care about, which is part of why Fastify's compiled-router approach outperforms Express under very high request rates - see Fastify vs Express ADR for the trade-off in full.
Security in this model is entirely a matter of what's in the chain and in what order: Express ships no CORS handling, no rate limiting, and no security headers by default, trusting the ecosystem (helmet, cors, express-rate-limit) to fill those in - see Security Middleware.
Observability follows the same philosophy - there's no built-in request logging or tracing, just an app.use() slot near the top of the chain where a logging middleware (or an OpenTelemetry instrumentation) can sit and see every request pass through.
Framework's core abstraction
Strength
Weakness
Best Fit
Express: flat middleware chain
Simple mental model; enormous ecosystem of drop-in middleware
No encapsulation; linear cost per layer; easy to misorder
Most CRUD APIs; teams valuing ecosystem breadth over structure
Fastify: encapsulated plugin tree
Isolated scopes; schema-compiled dispatch for speed
"A Router isolates its middleware the way a Fastify plugin does." It only scopes which requests reach a given set of layers by path prefix - it shares the same global prototype extensions and any mutated shared state with the rest of the app.
"Middleware order is a style preference." It's a correctness requirement - a body parser registered after a route that reads req.body will see undefined, because the chain has already passed that point by the time the parser would run.
"Calling next() skips to the next matching route, not the next middleware." It advances to the very next layer in registration order, matched independently for every layer - it has no awareness of "routes" as a distinct concept from middleware.
"Error handling middleware runs automatically when a handler throws." Only next(err) (or, in Express 5, an automatically-forwarded rejected Promise) routes into error middleware - a synchronous throw inside older non-async patterns can crash the process instead if not caught.
"Express is a full-featured framework like Rails or Django." It's deliberately minimal - routing and dispatch only; validation, ORM integration, and structure are all left to the ecosystem or to something built on top, like NestJS.
What is Express's one core abstraction, in a sentence?
An ordered chain of (req, res, next) functions, all sharing the same request and response objects, walked in registration order for every incoming request.
Where did the middleware chain idea come from?
Express is built directly on Connect, an earlier Node library that introduced the same linear middleware-chain concept - Express added routing on top without changing that underlying execution model.
How does Express actually dispatch a request internally?
It walks an ordered internal list of "layers" (each a path/method matcher plus a handler), running each one that matches until a layer ends the response or the list is exhausted - it's not a tree lookup, it's a linear scan.
Does a `Router` create an isolated scope like a Fastify plugin?
No - it only scopes which requests reach its layers, by path prefix. Prototype extensions and any shared mutable state still apply across the whole app, unlike Fastify's genuinely encapsulated plugin contexts.
Why does middleware order matter so much in Express?
Because the chain is strictly linear and stateful - a middleware later in the chain can only see effects (like a parsed body, or an attached user ID) from middleware that ran before it, never after.
How does `next(err)` actually change dispatch?
It skips every remaining normal-signature layer and jumps directly to the nearest four-argument error-handling middleware - which is why those handlers must be registered last, as the fallback destination for that jump.
What changed about error handling in Express 5?
Rejected Promises returned from async route handlers are now forwarded into next(err) automatically. In Express 4, an unhandled rejection inside an async handler was silently swallowed unless you added extra tooling or wrapped every handler manually.
Why does Express perform worse than Fastify at very high request rates?
Every layer in the chain adds a function call and a match check per request, even for requests that layer ultimately ignores - a cost proportional to chain length. Fastify compiles its routing and serialization ahead of time, avoiding much of that per-request overhead.
When should I not reach for Express's middleware model?
When you need genuine isolation between feature modules (favor Fastify's plugin encapsulation) or an enforced, testable architecture at large team scale (favor NestJS's DI module graph) - Express's flat, shared-state chain works against both goals by design.
Is Express opinionated about project structure?
No, deliberately not. It provides dispatch and routing only; body parsing beyond the built-ins, validation, database access, and file layout are all left to the developer or to ecosystem packages.
Why do people say Express is "just Connect with routing"?
Because that's close to literally true - Express reuses Connect's middleware-chain execution model and adds app.get/app.post/route-parameter matching on top, rather than inventing a different dispatch mechanism.
Does this model make Express unsuitable for large applications?
Not inherently - many large production APIs run on Express successfully - but the lack of enforced structure means the team has to impose its own conventions (routers per domain, service layers) rather than getting them from the framework.