RepoFold

Routing

Routing in Express determines how an application responds to a client request at a particular endpoint (URI path) and HTTP method. Each route can have one or more handler functions that are executed when the route matches. This page covers the core routing mechanisms provided by the Express core library, including route definition, route parameters, the Router class, and strategies for organizing routes across multiple files.

Purpose

The routing module provides the fundamental mechanism for mapping incoming HTTP requests to specific handler functions based on the request's method and path. It enables developers to build RESTful APIs and web applications by defining clear, declarative endpoints. The module supports:

  • Route definitions for all standard HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
  • Dynamic route parameters captured from the URL path
  • Multiple handler functions per route (middleware chains)
  • Sub-routing via the Router class for modular organization
  • Route-level middleware for authentication, validation, and other cross-cutting concerns

How It Works (Architecture)

The routing system is built on top of the Router class, which is itself a middleware function and a mini-application. The core express() application creates a main router that handles all incoming requests. When a request arrives, the application iterates through its internal layer stack, matching each layer's path pattern against the request URL. Layers that match are executed in order.

Layer Stack

Internally, Express maintains a stack of Layer objects. Each layer stores:

  • A path pattern (string or regular expression)
  • A method (or undefined for middleware)
  • A handler function (or array of handlers)
  • Route parameters metadata

When a route is defined using app.get(), app.post(), or similar methods, Express creates a new Route object and a new Layer that wraps that route. The layer is pushed onto the router's stack. The Route object itself maintains its own stack of layers, one per handler function.

Request Matching Process

  1. Incoming request arrives at the application
  2. Express iterates through the router's layer stack
  3. For each layer, it checks if the request method matches (if the layer has a method restriction)
  4. It checks if the request path matches the layer's path pattern
  5. If both match, the layer's handler is executed
  6. If the handler calls next(), Express continues to the next matching layer
  7. If no layer matches, Express sends a 404 response

Route Parameters

Route parameters are named segments in the URL path that are captured and stored in req.params. Express uses a simple pattern matching system: :paramName in the path string becomes a capture group in the internal regular expression. The captured values are decoded from URL encoding and assigned to req.params as key-value pairs.

Walkthrough of Main Flows

Defining Routes for Different HTTP Methods

Express provides convenience methods on the application object for each HTTP method. These methods are defined in lib/router/route.js and are dynamically generated from a list of HTTP verbs.

The method list is defined in lib/router/route.js:

// lib/router/route.js:21-42
var methods = require('methods');
//...
methods.forEach(function(method){
 Route.prototype[method] = function(){
 var handles = flatten(slice.call(arguments));
 for (var i = 0; i < handles.length; i++) {
 var handle = handles[i];
 if (typeof handle!== 'function') {
 var type = toString.call(handle);
 var msg = 'Route.' + method + '() requires a callback function but got a ' + type;
 throw new Error(msg);
 }
 debug('%s %o', method, this.path);
 var layer = Layer('/', {}, handle, this.methods[method]);
 layer.method = method;
 this.stack.push(layer);
 }
 return this;
 };
});

Each method creates a new Layer for each handler function and pushes it onto the route's internal stack. The layer is configured with the specific HTTP method, so it will only match requests using that method.

To define a route for a specific HTTP method, call the corresponding method on the application object:

// GET route
app.get('/users', function(req, res) {
 res.send('List of users');
});

// POST route
app.post('/users', function(req, res) {
 res.send('Create user');
});

// PUT route
app.put('/users/:id', function(req, res) {
 res.send('Update user ' + req.params.id);
});

// DELETE route
app.delete('/users/:id', function(req, res) {
 res.send('Delete user ' + req.params.id);
});

The app.all() method is special: it matches all HTTP methods for a given path. This is useful for defining middleware that should apply to all methods at a specific path.

app.all('/api/*', function(req, res, next) {
 console.log('API request received');
 next();
});

Using Route Parameters

Route parameters are defined by prefixing a colon (:) to the parameter name in the path string. Express captures the value from the URL and makes it available in req.params.

The parameter parsing logic is in lib/router/index.js within the handle method:

// lib/router/index.js:136-145
if (param) {
 var key = param.keys[i];
 var val = decode_param(param);
 if (key) {
 params[key.name] = val;
 }
}

Express supports several parameter patterns:

Simple parameter: /users/:userId captures a single segment

Optional parameter: /users/:userId? makes the parameter optional

Multiple parameters: /users/:userId/posts/:postId captures multiple values

Regular expression parameters: /users/:userId(\\d+) restricts the parameter to digits only

When a request matches a route with parameters, Express populates req.params:

app.get('/users/:userId/books/:bookId', function(req, res) {
 // For request: GET /users/42/books/123
 // req.params.userId === '42'
 // req.params.bookId === '123'
 res.json(req.params);
});

Express also supports special parameter handling through app.param(). This method allows you to define callback functions that execute when a specific parameter name appears in a route:

app.param('userId', function(req, res, next, id) {
 // Validate or transform the parameter
 req.user = users[id];
 if (!req.user) {
 return res.status(404).send('User not found');
 }
 next();
});

app.get('/users/:userId', function(req, res) {
 // req.user is already populated by the param callback
 res.json(req.user);
});

The app.param() method is defined in lib/router/index.js:

// lib/router/index.js:303-325
proto.param = function param(name, fn) {
 //...
 if (Array.isArray(name)) {
 name.forEach(function(key) {
 param.call(this, key, fn);
 }, this);
 return this;
 }
 //...
 (this.params || (this.params = {}))[name] = fn;
 return this;
};

Using the Router Class

The Router class is a standalone middleware that can contain its own routes and middleware. It is defined in lib/router/index.js and provides the same routing capabilities as the main application.

Creating a router:

var express = require('express');
var router = express.Router();

// Define routes on the router
router.use(function timeLog(req, res, next) {
 console.log('Time:', Date.now());
 next();
});

router.get('/users', function(req, res) {
 res.send('Users page');
});

router.get('/users/:id', function(req, res) {
 res.send('User ' + req.params.id);
});

The Router constructor is defined in lib/router/index.js:

// lib/router/index.js:30-50
var proto = module.exports = function(options) {
 var opts = options || {};
 function router(req, res, next) {
 router.handle(req, res, next);
 }
 // mixin Router class functions
 router.__proto__ = proto;
 router.params = {};
 router._params = [];
 router.caseSensitive = opts.caseSensitive;
 router.mergeParams = opts.mergeParams;
 router.strict = opts.strict;
 router.stack = [];
 return router;
};

The Router supports several options:

  • caseSensitive: Enable case sensitivity (default: disabled, so /Foo and /foo are treated the same)
  • mergeParams: Preserve req.params values from the parent router (default: false)
  • strict: Enable strict routing (default: disabled, so /foo and /foo/ are treated the same)

Routers can be mounted on specific paths using app.use():

var birds = require('./birds');
app.use('/birds', birds);

When mounted, the router's routes are relative to the mount path. For example, if the birds router defines a route at /, it responds to /birds/. If it defines a route at /about, it responds to /birds/about.

Organizing Routes Across Multiple Files

The Router class is the primary mechanism for organizing routes across multiple files. Each file exports a router that defines a subset of the application's routes.

Example: birds.js (routes file)

var express = require('express');
var router = express.Router();

// Middleware specific to this router
router.use(function(req, res, next) {
 console.log('Birds request:', req.method, req.url);
 next();
});

// Define routes
router.get('/', function(req, res) {
 res.send('Birds home page');
});

router.get('/about', function(req, res) {
 res.send('About birds');
});

module.exports = router;

Example: app.js (main application file)

var express = require('express');
var app = express();
var birds = require('./birds');

// Mount the birds router at /birds
app.use('/birds', birds);

app.listen(3000);

This pattern allows for clean separation of concerns. Each route file can have its own middleware, error handlers, and helper functions. The main application file only needs to mount the routers.

For larger applications, you can nest routers:

// api/v1/users.js
var router = express.Router();
router.get('/', function(req, res) {
 res.json({ users: [] });
});
module.exports = router;

// api/v1/index.js
var router = express.Router();
router.use('/users', require('./users'));
router.use('/posts', require('./posts'));
module.exports = router;

// app.js
var app = express();
app.use('/api/v1', require('./api/v1'));

Route Handler Chains

Routes can have multiple handler functions. Express executes them in sequence, passing control to the next handler when next() is called:

app.get('/example/a', function(req, res, next) {
 console.log('First handler');
 next();
}, function(req, res) {
 console.log('Second handler');
 res.send('Hello from B!');
});

This is useful for validation, authentication, or data loading that should happen before the main response handler.

Public API

Application Routing Methods

SymbolSignaturePurpose
app.METHODapp.METHOD(path, callback [, callback...])Define a route for a specific HTTP method (GET, POST, PUT, DELETE, etc.)
app.allapp.all(path, callback [, callback...])Define a route that matches all HTTP methods
app.paramapp.param(name, callback)Define callback for route parameter validation/transformation
app.routeapp.route(path)Create a chainable route handler for a path

Router Class

SymbolSignaturePurpose
express.Routerexpress.Router([options])Create a new router object
router.METHODrouter.METHOD(path, callback [, callback...])Define a route on the router for a specific HTTP method
router.allrouter.all(path, callback [, callback...])Define a route on the router that matches all HTTP methods
router.paramrouter.param(name, callback)Define callback for route parameter validation/transformation on this router
router.routerouter.route(path)Create a chainable route handler for a path on this router
router.userouter.use([path], callback [, callback...])Mount middleware or sub-routers at a path

Route Object

SymbolSignaturePurpose
route.METHODroute.METHOD(callback [, callback...])Add a handler for a specific HTTP method to this route
route.allroute.all(callback [, callback...])Add a handler that matches all HTTP methods to this route

Router Options

OptionTypeDefaultDescription
caseSensitiveBooleanfalseEnable case-sensitive routing (/Foo and /foo are different)
mergeParamsBooleanfalsePreserve req.params from parent router
strictBooleanfalseEnable strict routing (/foo and /foo/ are different)

Interactions with Other Modules

Application Module

The Application module creates a main router internally and delegates all routing to it. When you call app.get(), app.post(), or app.use(), the application calls the corresponding method on its internal router. The application also provides the app.param() method, which delegates to the router's param() method.

Middleware Module

Routes are a specialized form of middleware. The Router class itself is middleware, meaning it can be mounted using app.use(). Route handlers receive the same req, res, and next arguments as middleware. This allows route handlers to participate in the middleware chain, calling next() to pass control to the next matching route or middleware.

Request and Response Module

Route handlers receive the req (Request) and res (Response) objects. The routing system populates req.params with captured route parameters. Route handlers typically end the request-response cycle by calling methods on the response object (e.g., res.send(), res.json(), res.render()).

View System Module

Routes can render views using res.render(). The route handler passes data to the view template, and the view system generates the HTML response. This is commonly used in server-rendered applications where routes map to pages.

Configuration and Conventions

Path Patterns

Express uses path-to-regexp for matching route paths. The following patterns are supported:

  • :param - Named parameter
  • :param? - Optional parameter
  • * - Wildcard (matches any path)
  • (regex) - Regular expression constraint

Route Order Matters

Routes are matched in the order they are defined. More specific routes should be defined before less specific ones:

// Good: specific routes first
app.get('/users/new', handler);
app.get('/users/:id', handler);

// Bad: generic route catches all
app.get('/users/:id', handler);
app.get('/users/new', handler); // This will never match

Error Handling in Routes

Route handlers can pass errors to Express by calling next(err). Express will skip all remaining routes and middleware and go to the error handler:

app.get('/users/:id', function(req, res, next) {
 User.findById(req.params.id, function(err, user) {
 if (err) {
 return next(err); // Pass to error handler
 }
 if (!user) {
 return next(new Error('User not found'));
 }
 res.json(user);
 });
});

Route Naming Conventions

While Express does not enforce naming conventions, common practices include:

  • Use plural nouns for resource collections: /users, /posts
  • Use singular nouns for specific resources: /users/:id
  • Use kebab-case for multi-word paths: /blog-posts
  • Version APIs in the path: /api/v1/users
  • Use lowercase paths consistently

Performance Considerations

  • Route matching is O(n) where n is the number of routes. For very large applications, consider using a router that supports prefix-based matching.
  • Use app.param() sparingly as it runs for every route containing that parameter.
  • Group related routes under a single Router to reduce the number of top-level routes.
  • Use next('route') to skip to the next route if the current route cannot handle the request.
Generated from commit ae6dd37