EXPRESS.JS / v5.x architecture
Source-level breakdown · v5.x

Express.js, mapped from the inside out.

Not the tutorial mental model. This is how the actual repository is organized — files in lib/, the runtime composition, the routing stack, and the full request lifecycle, rendered as the system it really is.

§ 01 Express actual library structure (source-level)

This is roughly how the core repo is structured.

Repository layout · lib/
express/                      # the published package
├── index.js                  # 11 lines — re-exports lib/express.js
├── lib/
│   ├── express.js            # 81 loc   — createApplication() factory
│   ├── application.js        # 631 loc  — the app prototype (public API)
│   ├── request.js            # 514 loc  — getters spliced above IncomingMessage
│   ├── response.js           # 1053 loc — send · json · redirect · sendFile
│   ├── view.js               # 205 loc  — template resolution
│   └── utils.js              # 271 loc  — etag, negotiation, param merge
└── package.json              # 28 runtime dependencies

router/                       # a DEPENDENCY in express 5, not a folder
├── index.js                  # 748 loc  — Router: the stack and handle()
└── lib/
    ├── layer.js              # 247 loc  — Layer: compile a path, match()
    └── route.js              # 242 loc  — Route: handlers grouped by verb

Correction worth making early. Almost every Express diagram — including version 4 of this page — shows lib/router/ inside the package. In Express 5 the router was extracted into the standalone router package, and both lib/express.js and lib/application.js reach it through require('router'). Line counts above are measured from express@5.2.1 and router@2.2.0 as published.

§ 02 High-level architecture (how files connect)

The actual composition between the source files. express() is a factory; everything else hangs off the application object it produces.

Module composition graph
express() lib/express.js Application Object (app) lib/application.js Request lib/request.js Response lib/response.js Router (lazy) router (package) Middleware Stack Route Matching Handler
Express is just a function (app) + a middleware stack (router) + augmented req/res objects

§ 03 Core modules breakdown

Each file in lib/ has a single, well-defined responsibility. Here's what lives where, in actual structural form.

lib/ — module responsibilities at a glance
app application.js express.js factory · createApplication() mixin EventEmitter request.js extends IncomingMessage req.params · req.query req.body · req.get() req.accepts() response.js extends ServerResponse res.send() · res.json() res.status() · res.redirect() res.render() router/ index · route · layer stack of Layers match · dispatch METHOD() · use() view.js template / view handling render() utils.js helpers (shared)

3.1 lib/express.js → Application Factory

createApplication()
    ├── creates function: app(req, res, next)
    ├── mixes in:
    │     • EventEmitter
    │     • application prototype
    ├── attaches:
    │     • app.request prototype
    │     • app.response prototype

👉 This is why:

const app = express()

is actually a function you can pass to http.createServer().

3.2 lib/application.js → The real "app"

This is where most APIs live:

app
Public API surface
  • app.use() — add middleware
  • app.get() — define routes
  • app.post()
  • app.listen()
  • app.set() / get() — config
  • app.handle() — ENTRY POINT
app.handle(req, res)
Internal responsibility
  • setup req/res prototypes
  • attach res.locals
  • set headers
  • delegate → router.handle()

👉 app.handle() is the true execution entrypoint.

3.3 lib/request.js → req extension

Extends Node's:

http.IncomingMessage

Adds:

req
├── req.params
├── req.query
├── req.body
├── req.get()
├── req.accepts()

3.4 lib/response.js → res extension

Extends:

http.ServerResponse

Adds:

res
├── res.send()
├── res.json()
├── res.status()
├── res.redirect()
├── res.render()

3.5 Router system (the router package)

This is the REAL core of Express — and in version 5 it is not even inside the package. Router, Route and Layer live in router@2.x, which express depends on; path-to-regexp is in turn a dependency of that. Three files, 1237 lines, and every routing behaviour on this page.

§ 04 Router internals (the real engine)

Router · structural decomposition
Router
├── stack: Layer[]
│
├── use()        → adds middleware Layer
├── METHOD()     → creates Route → wraps in Layer
└── handle()     → iterates stack

4.1 Layer (smallest unit)

Layer
├── path
├── match(path)
├── handle(req, res, next)

👉 Think: "One executable step in the pipeline"

4.2 Route (groups handlers per HTTP method)

Route (/users)
├── stack: Layer[]
├── get()
├── post()
└── dispatch()

4.3 Router (orchestrator)

Router
├── stack: [
│     Layer("/", middleware),
│     Layer("/users", Route),
│ ]

Execution:

for each layer in stack:
    if match:
        execute handler
        → next()

👉 This is literally a linked middleware pipeline.

Router · Route · Layer relationship
Router stack: Layer[] Layer "/" middleware path = "/" match(p) handle(req,res,next) Layer "/users" wraps a Route Route("/users") stack: Layer[] · dispatch() Layer · GET handle = getUsersHandler method-scoped Layer · POST handle = createUserHandler method-scoped Layer · PUT handle = updateHandler method-scoped Layer · DELETE handle = deleteHandler method-scoped ↓ stack iteration ↓

§ 05 FULL request lifecycle (accurate flow)

Step through the lifecycle. Each click advances one stage along the real path a request takes from Node's HTTP server to res.send().

step 1 / 13
Request lifecycle · interactive
HTTP Request Node HTTP Server app(req, res) ← express() function app.handle() ├── set prototypes ├── init locals └── call router Router.handle() iterate Layer stack Layer match? regex.test(path) yes ↓ execute middleware next() Route matched? method + path yes ↓ Route.dispatch() handler(req, res) res.send() · Response sent

§ 06 Middleware system (how it fits structurally)

Middleware is not a "separate thing".

It is literally:

Layer {
    handle: function(req, res, next)
}

Types:

app.use()
app-level
  • runs for every request
router.use()
router-level
  • scoped to a sub-router
route.get(...)
route-level
  • per HTTP method
(err, req, res, next)
error-handling
  • 4 args → treated as error handler
express.json()
built-in
  • express.static()
  • express.json()

👉 All of them = just Layers in a stack.

§ 07 Putting EVERYTHING together (true mental model)

Express · top-down
createApplication()
Application (app)
Lazy Router
Stack of Layers
Each Layer = middleware OR route
Route = stack of method-specific Layers

Final compressed model (lock this in)

express()
  → app (function)

app
  → delegates to router

router
  → stack of layers

layer
  → executes middleware or route

route
  → executes method handlers
Express is NOT a framework with controllers, not MVC, not magic routing.

It is a programmable pipeline of functions over HTTP, implemented as a stack of Layer objects.

§ 08 Routing internals — path-to-regexp, params, matching

This is where Express stops being "simple" and becomes clever string → regex machinery.

8.1 How paths actually work

When you write:

app.get("/users/:id", handler)

Express does NOT store /users/:id as a string.

👉 It compiles it using path-to-regexp into something like:

^\/users\/([^\/]+?)\/?$

And stores metadata:

{
  keys: [{ name: "id" }],
  regexp: /^\/users\/([^\/]+?)\/?$/
}
Path compilation pipeline
input string "/users/:id" compiler path-to-regexp parses tokens · builds regex stored metadata keys: [{ name: "id" }] regexp: /^\/users\/([^\/]+?)\/?$/

8.2 Matching flow inside a Layer

Layer.match(path)
   ↓
regexp.test(path)
   ↓
if match:
   extract params

8.3 Param extraction

When request comes:

GET /users/42

Regex match produces:

["/users/42", "42"]

Mapping happens:

req.params = {
  id: "42"
}
Express routing = Regex engine + param extraction layer. NOT string matching.

8.4 Order matters more than you think

app.get("/users/:id", handlerA)
app.get("/users/profile", handlerB)

👉 /users/profile will hit handlerA unless ordered correctly.

Why?

/users/profile → matches /users/:id
Stack iteration · first-match-wins
GET /users/profile router.stack (top → bottom) Layer · /users/:id → handlerA ⚠ matches first → wins (id="profile") Layer · /users/profile → handlerB never reached handlerA fires

8.5 Nested routers (composition model)

app.use("/api", router)

Internally:

Layer("/api")
   ↓
strips prefix → "/users"
   ↓
passes to child router
Mount-point composition
/api/users/42 app router Layer "/api" strips "/api" child router sees: /users/42 Layer "/users/:id" → Route → handler req.params.id = "42" res.json

👉 Routers are stackable pipelines.

§ 09 Performance — why Express is "slower" than Fastify

Let's break this down at a systems level.

9.1 Express execution cost

Each request goes through:

for every Layer:
   check match
   call function
   manage next()

Overhead sources:

  • Function calls per middleware
  • Closure allocations
  • Dynamic typing
  • Regex matching per layer
  • No compilation step

9.2 Fastify's advantage

Fastify builds a precompiled routing tree.

Instead of:

O(n) → scan all layers

It does:

O(log n) → lookup in route tree

9.3 Visual comparison

// Express — linear scan
Request Layer 1 → match? Layer 2 → match? Layer 3 → match? Layer 4 → match?

cost grows linearly with stack depth

// Fastify — tree lookup
Request root /api /users /auth Direct handler

cost grows logarithmically · O(log n)

9.4 Serialization cost

Express:

res.json(data)
→ JSON.stringify()

Fastify:

  • Uses compiled serializers (schema-based)
  • Avoids runtime cost

9.5 Benchmarks reality

  • Express: flexible, dynamic
  • Fastify: structured, optimized

👉 Difference matters at:

10k–100k req/sec scale

NOT typical apps.

Express trades performance for flexibility and simplicity.
Fastify trades flexibility for speed.

§ 10 Production architecture — avoiding middleware chaos

This is where most Express apps fail.

10.1 The common mistake

app.use(auth)
app.use(validate)
app.use(log)
app.use(rateLimit)
app.use(...)

👉 Becomes:

UNCONTROLLED GLOBAL PIPELINE

10.2 Proper layered architecture

You want bounded pipelines per route group:

Bounded pipelines · per route group
app
 ├── /auth
 │     ├── validation
 │     └── controller
 │
 └── /users
       ├── auth
       ├── validation
       └── controller

10.3 Clean folder structure

src/
├── routes/
│   ├── user.routes.js
│   └── auth.routes.js
│
├── controllers/
├── services/
├── middleware/
└── db/

10.4 Route-level composition (correct way)

router.post(
  "/claim",
  validateInput,
  rateLimit,
  async (req, res) => {
    const voucher = await voucherService.create(req.body);
    res.json(voucher);
  }
);

10.5 Service layer separation

Controller
HTTP concerns
  • parse req · format res
  • status codes · errors
Service
Business logic
  • orchestrate workflow
  • domain rules
DB
Persistence
  • queries · transactions
  • data shape

10.6 Example (your voucher system)

POST /claim · full pipeline
POST /claim [Validation Middleware] [Rate Limit Middleware] [Controller] [Service] ├── generate voucher ├── save to DB └── send WhatsApp Response

10.7 Error handling (critical)

app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

Internally:

Layer with 4 args → treated as error handler
Middleware should be:
  • Scoped (not global)
  • Ordered intentionally
  • Minimal per request

§ 11 FINAL — Unified deep mental model

End-to-end · request → response
Incoming Request Node HTTP Server Express App (function) Router Stack of Layers Each Layer: ├── match via regex ├── extract params ├── execute handler └── call next() Route handler Service layer Database / external APIs Response

The real "senior-level" understanding (lock this in)

Express is:

  1. A request dispatcher (router)
  2. A function pipeline (middleware)
  3. A thin HTTP abstraction (req/res)
Everything else in your backend is your architecture on top of it.

§ 12 Every breakdown on this page

Each section, subsection and diagram on this page opens a breakdown: prose plus interactive figures that run a real routing engine rather than describing one.

Back to JIT-Atlas