This page traces one HTTP request end to end through the actual code paths in lib/. Two facts make the whole design legible: an Express app is a function with the Node request-handler signature, and everything after app.handle is delegation -- to the router package for dispatch and to finalhandler when nothing else responds.

The app is the server callback

createApplication() in lib/express.js builds a plain function and mixes the application prototype onto it:

lib/express.js
function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);
  // …
  app.init();
  return app;
}

app.listen is then a three-line convenience: create a Node http.Server with the app as its callback and listen. (The extra lines wrap a final callback with once so it also fires on server errors such as EADDRINUSE -- a guarantee tested in test/app.listen.js.)

lib/application.js
app.listen = function listen() {
  var server = http.createServer(this)
  var args = slice.call(arguments)
  if (typeof args[args.length - 1] === 'function') {
    var done = args[args.length - 1] = once(args[args.length - 1])
    server.once('error', done)
  }
  return server.listen.apply(server, args)
}

Because the app is just a handler function, you can also pass it to https.createServer({...}, app) or mount it inside another app -- nothing in Express assumes it owns the server.

One request, start to finish

Every node in this diagram is a call you can find in the code: http.Server invokes the app function, which runs app.handle (lib/application.js), which delegates to router.handle from the router package, which walks the layer stack until a handler ends the response or control falls through to finalhandler.

sequenceDiagram
    participant C as Client
    participant S as http.Server
    participant A as app.handle (lib/application.js)
    participant R as router.handle (router pkg)
    participant M as middleware layers
    participant H as route handler
    participant F as finalhandler

    C->>S: HTTP request
    S->>A: app(req, res)
    A->>A: set X-Powered-By, setPrototypeOf(req, res)
    A->>R: this.router.handle(req, res, done)
    R->>M: matching layers, in registration order
    M->>M: next() advances / next(err) skips to arity-4
    M->>H: first matching route handler
    alt handler responds
        H->>C: res.send / res.json / res.end
    else nothing matched or next(err) fell through
        R->>F: done(err)
        F->>C: 404 or error status
    end

Inside app.handle

app.handle does four things before handing off: build the fallback done callback, set the X-Powered-By header, swap the prototypes of Node's raw req/res objects for Express's extended ones, and call the router.

lib/application.js
app.handle = function handle(req, res, callback) {
  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });
  // …
  // alter the prototypes
  Object.setPrototypeOf(req, this.request)
  Object.setPrototypeOf(res, this.response)
  // …
  this.router.handle(req, res, done);
};

The prototype swap is the entire mechanism behind req.query, res.json, and every other Express extension -- covered in request and response extensions.

The callback parameter is what makes apps nestable: when an app is mounted inside another, the parent passes a next function here, so errors and misses continue in the parent instead of hitting finalhandler. Only the outermost app creates the finalhandler fallback.

Dispatch and the two exits

The router (see routing and middleware) walks its stack of layers -- path-matched middleware and routes -- calling each with (req, res, next). There are exactly two ways out:

  1. A handler responds. res.send, res.json, res.render, res.end -- the response is written and later layers never run.
  2. Control falls off the end. Every layer called next() (or next(err) and no error handler consumed it). The router calls done, and finalhandler writes a 404 -- or, for an error, the error's status with a body that is stack trace in development and status message in production (it reads the env option passed above).

Registration order is the only ordering rule. test/middleware.basic.js pins this: two app.use calls record ['one', 'two'], never the reverse. The error path -- how next(err) skips ordinary layers and finds four-argument handlers -- is specified in test/Route.js and walked through in routing and middleware.

Sources: lib/application.js, lib/express.js · last synced 2026-07-27 · a371447 · version 5.2.1