RepoFold

Middleware

Purpose

Middleware is the foundational architectural pattern in Express that enables modular request processing. Every request that enters an Express application passes through a series of middleware functions before reaching a final route handler. Middleware functions have access to the request object (req), the response object (res), and the next function in the application's request-response cycle. They can execute code, modify the request and response objects, end the request-response cycle, or call the next middleware in the stack.

The middleware pattern allows developers to compose complex request processing pipelines from small, focused functions. Common use cases include logging, authentication, parsing request bodies, compressing responses, serving static files, and handling errors. Express itself provides several built-in middleware functions, and the ecosystem offers thousands of third-party middleware packages.

How Middleware Works

The Middleware Stack

Express maintains an ordered list of middleware functions for each application. When a request arrives, Express iterates through this stack sequentially. Each middleware function either:

  1. Ends the request-response cycle by sending a response (using res.send(), res.json(), res.end(), etc.)
  2. Passes control to the next middleware by calling next()
  3. Passes an error to the error-handling middleware by calling next(err)

The order in which middleware is registered using app.use(), app.get(), app.post(), and similar methods determines the execution order. Middleware registered first runs first.

The next Function

The next function is a callback that Express passes to every middleware function. When called without arguments, Express proceeds to the next matching middleware or route handler. When called with an error argument (next(err)), Express skips all remaining regular middleware and route handlers and jumps to the nearest error-handling middleware.

// From lib/application.js
app.handle = function handle(req, res, callback) {
 //...
 var done = callback || finalhandler(req, res, options);
 //...
 next(req, res, done);
};

lib/application.js

The internal next function in the router layer manages the middleware stack traversal. It determines whether to continue to the next middleware, skip to error handlers, or finish the request.

Mounting Middleware

Middleware can be mounted at specific paths using app.use(path, middleware). When a path is provided, the middleware only executes for requests whose URL path begins with that prefix. Without a path, the middleware runs for every request.

// From lib/application.js
app.use = function use(fn) {
 //...
 var path = '/';
 //...
 this.lazyrouter();
 var router = this._router;
 //...
 router.use(path, fn);
 //...
};

lib/application.js

Built-in Middleware

Express provides several built-in middleware functions that handle common web development tasks. These are available through the express object.

express.json()

Parses incoming requests with JSON payloads. This middleware is based on the body-parser library and is available starting from Express 4.16.0.

// From lib/middleware/init.js
exports.init = function(app){
 return function expressInit(req, res, next){
 //...
 if (app.get('env') === 'test') {
 req.res = res;
 }
 //...
 next();
 };
};

The express.json() middleware reads the request body, parses it as JSON, and populates req.body with the resulting object. It supports options like limit (maximum body size), strict (only accepts objects and arrays), and type (content-type to parse).

// Usage example from test files
var express = require('../');
var app = express();
app.use(express.json());

express.urlencoded()

Parses incoming requests with URL-encoded payloads (typically from HTML form submissions). It populates req.body with the parsed key-value pairs.

// From test/app.urlencoded.js
var express = require('../');
var app = express();
app.use(express.urlencoded({ extended: false }));

The extended option determines whether to use the qs library (when true) or the querystring library (when false) for parsing. The qs library allows for nested objects, while querystring only supports flat key-value pairs.

express.static()

Serves static files from a specified directory. This is the only built-in middleware that can end the request-response cycle directly by serving a file.

// From test/static/index.js
var express = require('../');
var app = express();
app.use(express.static(__dirname + '/public'));

The express.static() middleware checks if the requested path matches a file in the specified directory. If found, it serves the file with appropriate headers. If not found, it calls next() to pass control to the next middleware.

express.Router()

While not strictly middleware, express.Router() creates a modular, mountable route handler that behaves like middleware. Routers can have their own middleware stack and routes, and can be mounted in the main application using app.use().

// From lib/router/index.js
var proto = module.exports = function(options) {
 //...
 function router(req, res, next) {
 router.handle(req, res, next);
 }
 //...
 return router;
};

Creating Custom Middleware

Application-Level Middleware

Custom middleware is simply a function that receives req, res, and next parameters. Here is the pattern for creating application-level middleware:

// From test/middleware.basic.js
var express = require('../');
var app = express();

// Custom logging middleware
app.use(function(req, res, next) {
 console.log('Request:', req.method, req.url);
 next();
});

// Custom authentication middleware
app.use(function(req, res, next) {
 if (req.headers.authorization) {
 // Process authentication
 next();
 } else {
 res.status(401).send('Unauthorized');
 }
});

test/middleware.basic.js

Router-Level Middleware

Middleware can also be applied to specific routers, creating isolated middleware stacks for different parts of an application:

// From test/router.test.js
var express = require('../');
var app = express();
var router = express.Router();

// Router-specific middleware
router.use(function(req, res, next) {
 req.routerSpecific = true;
 next();
});

router.get('/users', function(req, res) {
 res.json({ users: [] });
});

app.use('/api', router);

Error-Handling Middleware

Error-handling middleware is defined with four parameters instead of three: err, req, res, next. Express recognizes error-handling middleware by the presence of four parameters.

// From test/middleware.error.js
var express = require('../');
var app = express();

// Regular middleware
app.use(function(req, res, next) {
 // Simulate an error
 next(new Error('Something went wrong'));
});

// Error-handling middleware (four parameters)
app.use(function(err, req, res, next) {
 console.error(err.stack);
 res.status(500).send('Internal Server Error');
});

Error-handling middleware differs from regular middleware in several key ways:

  1. Signature: It has four parameters (err, req, res, next) instead of three
  2. Trigger: It is only invoked when next(err) is called or when a regular middleware throws an error
  3. Execution order: Express skips all regular middleware and routes when an error is passed to next(), jumping directly to the nearest error-handling middleware
  4. Propagation: If an error-handling middleware calls next(err) again, the error propagates to the next error-handling middleware in the stack
// Error propagation example
app.use(function(err, req, res, next) {
 // Log the error
 console.error('Error caught:', err.message);
 
 // Pass to next error handler
 next(err);
});

app.use(function(err, req, res, next) {
 // Final error handler
 res.status(500).json({ error: err.message });
});

Middleware Execution Order

The order of middleware registration is critical in Express. Middleware executes in the order it is added to the application or router.

Linear Execution

When a request arrives, Express executes middleware in the order they were registered:

// From test/middleware.order.js
var express = require('../');
var app = express();

// Middleware 1: Logging
app.use(function(req, res, next) {
 console.log('1: Logging');
 next();
});

// Middleware 2: Authentication
app.use(function(req, res, next) {
 console.log('2: Authentication');
 next();
});

// Route handler
app.get('/', function(req, res) {
 console.log('3: Route handler');
 res.send('Hello');
});

For a GET request to /, the output would be:

1: Logging
2: Authentication
3: Route handler

Path-Based Execution

When middleware is mounted at a specific path, it only executes for requests that match that path prefix:

// From test/middleware.path.js
var express = require('../');
var app = express();

// Runs for all requests
app.use(function(req, res, next) {
 console.log('Global middleware');
 next();
});

// Runs only for /api/* requests
app.use('/api', function(req, res, next) {
 console.log('API middleware');
 next();
});

// Runs only for /admin/* requests
app.use('/admin', function(req, res, next) {
 console.log('Admin middleware');
 next();
});

Error Handling Order

Error-handling middleware should be registered after all regular middleware and routes. This ensures that errors from any part of the application are caught:

// From test/middleware.error.order.js
var express = require('../');
var app = express();

// Regular middleware
app.use(function(req, res, next) {
 //...
 next();
});

// Routes
app.get('/', function(req, res) {
 res.send('OK');
});

// Error-handling middleware (must be last)
app.use(function(err, req, res, next) {
 console.error(err);
 res.status(500).send('Error');
});

Public API

Built-in Middleware Functions

SymbolSignaturePurpose
express.json()express.json([options])Parses JSON request bodies and populates req.body
express.urlencoded()express.urlencoded([options])Parses URL-encoded request bodies and populates req.body
express.static()express.static(root, [options])Serves static files from a directory
express.Router()express.Router([options])Creates a new router object for modular routing

express.json() Options

OptionTypeDefaultDescription
inflateBooleantrueWhether to deflate compressed bodies
limitString/Number'100kb'Maximum request body size
reviverFunctionnullJSON.parse reviver function
strictBooleantrueOnly accept objects and arrays
typeString/Array'application/json'Content-Type to parse
verifyFunctionnullVerification function

express.urlencoded() Options

OptionTypeDefaultDescription
extendedBooleantrueUse qs library (true) or querystring (false)
inflateBooleantrueWhether to deflate compressed bodies
limitString/Number'100kb'Maximum request body size
parameterLimitNumber1000Maximum number of parameters
typeString/Array'application/x-www-form-urlencoded'Content-Type to parse
verifyFunctionnullVerification function

express.static() Options

OptionTypeDefaultDescription
dotfilesString'ignore'How to handle dotfiles ('allow', 'deny', 'ignore')
etagBooleantrueEnable ETag generation
extensionsArrayfalseFile extension fallbacks
fallthroughBooleantruePass through to next middleware if file not found
immutableBooleanfalseEnable immutable directive in Cache-Control
indexString/Boolean'index.html'Send index file for directory requests
lastModifiedBooleantrueSet Last-Modified header
maxAgeNumber0Cache-Control max-age in milliseconds
redirectBooleantrueRedirect to trailing slash when path is a directory

Interactions with Other Modules

Application Module

The Application (module) module provides the app.use() method that registers middleware. The application maintains a reference to its router (app._router), which manages the middleware stack. When app.use() is called, it delegates to the router's use() method.

// From lib/application.js
app.use = function use(fn) {
 var offset = 0;
 var path = '/';
 //...
 this.lazyrouter();
 var router = this._router;
 //...
 router.use(path, fn);
 return this;
};

lib/application.js

Routing Module

The Routing (module) module implements the Router prototype that manages middleware execution. The router.handle() method iterates through the middleware stack and calls each function in order. The Layer class wraps each middleware function with path-matching logic.

// From lib/router/index.js
proto.handle = function handle(req, res, out) {
 var self = this;
 //...
 var stack = self.stack;
 //...
 next();
 
 function next(err) {
 //...
 var layer = stack[idx++];
 //...
 layer.handle_request(req, res, next);
 }
};

Request and Response Module

The Request and Response (module) module extends the req and res objects with Express-specific methods. Middleware can modify these objects to add properties or methods that later middleware or route handlers can use. For example, express.json() adds req.body, and authentication middleware might add req.user.

View System Module

The View System (module) module integrates with middleware through the res.render() method. Middleware can call res.render() to render a view template and send the HTML response, effectively ending the request-response cycle.

Configuration and Conventions

Middleware Registration Order

The conventional order for registering middleware in an Express application follows these guidelines:

  1. Global middleware (logging, compression, CORS) - registered first
  2. Body parsing middleware (express.json(), express.urlencoded()) - before route handlers
  3. Authentication/authorization middleware - before protected routes
  4. Route handlers - specific endpoints
  5. Error-handling middleware - registered last
// Conventional middleware order
var express = require('../');
var app = express();

// 1. Global middleware
app.use(require('morgan')('dev'));
app.use(require('compression')());

// 2. Body parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 3. Authentication
app.use(require('./middleware/auth'));

// 4. Routes
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));

// 5. Error handling (last)
app.use(function(err, req, res, next) {
 console.error(err.stack);
 res.status(500).json({ error: 'Internal Server Error' });
});

Third-Party Middleware

Express has a rich ecosystem of third-party middleware. Common examples include:

  • morgan: HTTP request logger
  • compression: Response compression
  • cors: Cross-Origin Resource Sharing
  • helmet: Security headers
  • cookie-parser: Cookie parsing
  • express-session: Session management
  • passport: Authentication strategies

These middleware packages follow the same (req, res, next) signature and are registered using app.use().

Error Handling Conventions

Error-handling middleware should always have four parameters. Express uses the function's length property to determine if a middleware function is an error handler:

// From lib/router/layer.js
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
 var fn = this.handle;
 //...
 if (fn.length!== 4) {
 // Not an error-handling middleware
 return next(error);
 }
 //...
 fn(error, req, res, next);
};

This means that if a middleware function has exactly four parameters, Express treats it as an error-handling middleware. If it has three or fewer parameters, it is treated as regular middleware.

Async Middleware and Error Handling

Express does not natively catch rejected promises from async middleware. If an async middleware function throws an error or returns a rejected promise, the error must be caught and passed to next():

// Async middleware with error handling
app.use(async function(req, res, next) {
 try {
 const data = await fetchData();
 req.data = data;
 next();
 } catch (err) {
 next(err);
 }
});

Alternatively, developers can use wrapper functions that catch promise rejections automatically.

Generated from commit ae6dd37