Express Source Code Design Patterns
Express’s design and implementation incorporate various design patterns, enhancing the framework’s extensibility, flexibility, and maintainability while providing developers with powerful functionality and an elegant API.
Singleton Pattern: Application Object
The Express Application class employs the singleton pattern, ensuring only one Application instance exists throughout the application. This allows developers to access this instance directly via the express() function without worrying about duplicate instance creation.
function Application() {
// Initialize Application instance
}
// Bind the Application instance to the express object
const express = module.exports = new Application();Factory Pattern: Creating Middleware and Routes
Express uses the factory pattern to create middleware and routes. Methods like app.use(), app.get(), and app.post() essentially create and register different middleware and route handlers.
app.use(bodyParser.json()); // Create and register JSON parsing middleware
app.get('/users', (req, res) => { /* ... */ }); // Create and register GET /users route handlerObserver Pattern: Event Listening
Express leverages an event-listening mechanism (similar to the observer pattern) to handle certain asynchronous operations, such as error handling and request completion notifications.
app.on('mount', function() {
console.log('Application has been mounted.');
});Strategy Pattern: Error Handling Strategies
Express’s error-handling mechanism embodies the strategy pattern, allowing developers to register multiple error-handling functions, each employing different strategies based on error type and context.
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
res.status(401).send('Invalid token!');
} else {
next(err);
}
});Decorator Pattern: Extending Request and Response Objects
Express uses the decorator pattern to extend the http.IncomingMessage and http.ServerResponse objects, adding additional properties and methods like req.body, res.send, and res.json.
// Decorate req object
req.body = {}; // Add body property
req.query = {}; // Add query property
// Decorate res object
res.send = function(body) {
this.writeHead(200, { 'Content-Type': 'text/plain' });
this.end(body);
};Composite Pattern: Combining Routes and Middleware
Express’s routing and middleware system reflects the composite pattern, allowing developers to build complex request-handling logic through nesting and composition.
const userRouter = express.Router();
userRouter.get('/', (req, res) => {
res.send('List of users');
});
app.use('/users', userRouter);Module Pattern: Modular Design
Express’s modular design follows the module pattern, encapsulating different functionalities in independent modules like body-parser, cookie-parser, and express-session, making the framework easy to extend and maintain.
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');Proxy Pattern: Middleware as Proxy
Middleware in Express can be viewed as an application of the proxy pattern. Middleware intercepts requests before they reach the target handler, performing preprocessing tasks like authentication, logging, or error handling, then passing the request to the next middleware or handler.
app.use(function(req, res, next) {
// Perform preprocessing tasks, such as authentication
if (!req.user) {
return res.status(401).send('Unauthorized');
}
next(); // Pass request to the next middleware or handler
});Command Pattern: Encapsulating Asynchronous Operations
Asynchronous operations in Express, such as database queries or file operations, can be encapsulated using the command pattern for easier management and testing. While Express itself does not directly use the command pattern, developers can apply it in their middleware or route handlers.
class GetUserCommand {
constructor(userId) {
this.userId = userId;
}
execute() {
return User.findById(this.userId);
}
}
app.get('/users/:id', async (req, res, next) => {
const command = new GetUserCommand(req.params.id);
try {
const user = await command.execute();
res.json(user);
} catch (error) {
next(error);
}
});State Pattern: State Machine for Request Handling
While Express does not explicitly use the state pattern, complex request-handling logic can be treated as a state machine, with each middleware or route handler representing a state. The request transitions between states until processing is complete.
let state = 'INIT';
app.use((req, res, next) => {
if (state === 'INIT') {
state = 'AUTH';
authenticateUser(req, res, next);
} else {
next();
}
});
app.use((req, res, next) => {
if (state === 'AUTH') {
state = 'PROCESS';
processRequest(req, res, next);
} else {
next();
}
});
app.use((req, res) => {
if (state === 'PROCESS') {
state = 'COMPLETE';
completeRequest(req, res);
}
});Adapter Pattern: Supporting Different Data Sources
In Express, the adapter pattern can be used to handle data from various sources (e.g., databases, APIs, file systems). Adapters unify the interfaces of different data sources, allowing the application to process data consistently without concern for the specific source.
class DatabaseAdapter {
constructor(db) {
this.db = db;
}
find(query) {
return this.db.find(query);
}
save(data) {
return this.db.save(data);
}
}
const dbAdapter = new DatabaseAdapter(database);
app.use(async (req, res, next) => {
const data = await dbAdapter.find(req.query);
req.data = data;
next();
});Best Practices
- Modularity: Break the application into small, reusable modules, each responsible for a single responsibility to facilitate testing and maintenance.
- Dependency Injection: Pass external dependencies (e.g., database connections, configurations) as parameters to components rather than creating them internally, promoting decoupling and testability.
- Error Handling: Ensure every middleware and route handler properly handles errors and passes them to error-handling middleware.
- Logging: Use middleware to log request and response information for debugging and monitoring.
- Performance Optimization: Analyze the request-handling flow, identify bottlenecks, optimize database queries, and use caching to reduce redundant computations.



