In Express 5, route matching and dispatch live in the standalone router package (see the router dependency page for why). What remains in this repo is the registration API on app, and -- crucially -- the test files test/Router.js and test/Route.js, which are the authoritative statement of dispatch behavior. This page describes the model those tests pin down.
The model: a stack of layers
A router holds an ordered stack. Each app.use(path, fn) or route registration pushes a layer. Dispatch walks the stack top to bottom; a layer runs only if its path matches, and passing control is always explicit via next():
next()-- continue to the next matching layer.next(err)-- skip every ordinary layer; only error handlers (arity 4) run from here.next('route')-- skip remaining handlers of the current route, continue matching.- Not calling
nextand not responding -- the request hangs. Order of registration is the only priority rule.
app.use normalizes its arguments (default path /, arrays flattened to any depth -- test/app.use.js covers "nested arrays of middleware") and rejects non-functions early:
var fns = flatten.call(slice.call(arguments, offset), Infinity);
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}
test/Router.js pins the rejection cases individually: string, number, null, and Date as middleware all throw.
Routes: per-path handler stacks
app.get('/x', h) expands to this.route('/x').get(h) -- a Route is a mini-stack for one path, with one entry per method. Guarantees from test/Route.js:
.gethandlers do not run for HEAD-only registration mismatches ("should limit to just .VERB").- Multiple handlers on one route run in order and may fall through with
next()("should allow fallthrough"). .allruns for every method ("should handle VERBS").
app.all in this repo is a loop over every Node HTTP method calling route[method]; the method list itself comes from node:http:
exports.methods = METHODS.map((method) => method.toLowerCase());
Error handling: arity is the contract
A middleware with four declared parameters is an error handler; everything else is ordinary. The dispatch rule -- next(err) skips ordinary layers, runs the next arity-4 layer -- is pinned exactly in test/Route.js:
route.all(function(req, res, next){
next(new Error('foobar'));
});
route.all(function(req, res, next){
req.order += '0';
next();
});
route.all(function(err, req, res, next){
req.order += 'a';
next(err);
});
route.dispatch(req, {}, function (err) {
assert.ok(err)
assert.strictEqual(err.message, 'foobar')
assert.strictEqual(req.order, 'a')
done();
});
req.order ends as 'a' -- the ordinary layer between the throw and the handler never ran. Synchronous throw inside a handler is converted to next(err) by the router ("should handle throw", including inside error handlers themselves). An error that no arity-4 layer consumes reaches finalhandler (see the request lifecycle) and becomes the response status.
Because error handlers must come after the layers whose errors they catch, examples/error/index.js registers its handler last, with the comment "if it were above it would not receive errors".
Params: named-segment callbacks
app.param(name, fn) registers a callback that runs when a route with :name matches, before the route's handlers. From examples/params/index.js -- validation that converts or rejects before any handler sees the request:
app.param(['to', 'from'], function(req, res, next, num, name){
req.params[name] = parseInt(num, 10);
if( isNaN(req.params[name]) ){
next(createError(400, 'failed to parseInt '+num));
} else {
next();
}
});
(The array form is the one piece of param handling implemented in this repo -- lib/application.js loops and forwards each name to router.param.) The key dispatch guarantee, from test/Router.js: a param callback runs once per request per value, even when multiple nested routers match the same segment ("should only call once per request"), and runs again only "when values differ".
Routers compose
express.Router() creates a standalone router you can mount with app.use(prefix, router) -- the pattern in examples/multi-router:
app.use('/api/v1', require('./controllers/api_v1'));
app.use('/api/v2', require('./controllers/api_v2'));
The mount prefix is stripped from req.url for the inner router (pinned in test/app.use.js, "should strip path from req.url") and restored afterwards, which is why an inner router can be written as if it owned /. Whole apps mount the same way, with the extra bookkeeping described in the application object.
Depth and volume are tested, not assumed: test/Router.js runs 6,000 registered routes and 6,000-layer synchronous stacks to guarantee no stack overflow, and a parallel-requests test guarantees per-request isolation of req.params.