Express Design Patterns
Introduction
Design patterns are reusable solutions to common problems in software design. When building web applications with Express.js, implementing the right design patterns can significantly improve your code organization, maintainability, and scalability.
This guide introduces key design patterns for Express applications, helping beginners structure their code in a way that follows industry best practices. You'll learn patterns ranging from basic middleware usage to more complex architectural designs like MVC (Model-View-Controller).
Why Use Design Patterns in Express?
Before diving into specific patterns, let's understand why they're important:
- Code Organization: Keep your codebase structured and readable
- Separation of Concerns: Divide your application into logical components
- Maintainability: Make updates and bug fixes easier
- Scalability: Allow your application to grow without becoming unwieldy
- Team Collaboration: Enable multiple developers to work on different parts of the application
1. The Middleware Pattern
Express is built around the middleware pattern, which is a chain of processing functions that have access to the request, response objects, and the next middleware function.
Basic Middleware Structure
app.use((req, res, next) => {
// Middleware logic goes here
console.log('Request received at:', new Date().toISOString());
next(); // Pass control to the next middleware
});
Example: Request Logger Middleware
// Creating a simple logging middleware
const requestLogger = (req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next();
};
// Using the middleware
const express = require('express');
const app = express();
app.use(requestLogger);
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
When you visit the homepage, the console will output something like:
GET / - 2023-11-08T14:55:23.453Z
Practical Application: Error Handling Middleware
Error handling middleware has a special signature with four parameters:
// Regular middleware
app.use((req, res, next) => {
// Some code that might throw an error
if (!req.user) {
next(new Error('User not found'));
}
next();
});
// Error handling middleware (notice the 4 parameters)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send({
error: {
message: err.message || 'Something went wrong!',
status: 500
}
});
});
2. The Router Pattern
Express routers allow you to modularize your routes and make your application more maintainable.
Basic Router Structure
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.send('Router home page');
});
router.get('/about', (req, res) => {
res.send('About page');
});
module.exports = router;
Example: Modular Routing
// users.routes.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.send('List of users');
});
router.get('/:id', (req, res) => {
res.send(`User details for ID: ${req.params.id}`);
});
module.exports = router;
// app.js
const express = require('express');
const app = express();
const userRoutes = require('./users.routes');
app.use('/users', userRoutes);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
When you visit /users/123
, the application will respond with "User details for ID: 123".
3. The MVC Pattern
Model-View-Controller (MVC) is a widely used architectural pattern that helps separate concerns in your application.
Directory Structure
/project-root
/controllers
userController.js
/models
userModel.js
/views
userView.ejs
/routes
userRoutes.js
app.js
Example: MVC Implementation
// models/userModel.js
const users = [
{ id: 1, name: 'John Doe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', email: '[email protected]' }
];
module.exports = {
findAll: () => users,
findById: (id) => users.find(user => user.id === parseInt(id))
};
// controllers/userController.js
const User = require('../models/userModel');
module.exports = {
getAllUsers: (req, res) => {
const users = User.findAll();
res.render('users', { users });
// Or for API: res.json(users);
},
getUserById: (req, res) => {
const user = User.findById(req.params.id);
if (user) {
res.render('userDetail', { user });
// Or for API: res.json(user);
} else {
res.status(404).send('User not found');
}
}
};
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
module.exports = router;
// app.js
const express = require('express');
const app = express();
const userRoutes = require('./routes/userRoutes');
app.set('view engine', 'ejs');
app.use('/users', userRoutes);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
For an API-focused application, you might skip the views and return JSON responses directly from controllers.
4. The Service Layer Pattern
Adding a service layer between controllers and models can improve separation of concerns, especially for complex business logic.
Example: Service Layer Implementation
// services/userService.js
const User = require('../models/userModel');
module.exports = {
getAllUsers: async () => {
try {
return await User.findAll();
} catch (error) {
throw new Error('Error fetching users');
}
},
getUserById: async (id) => {
try {
const user = await User.findById(id);
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
throw error;
}
}
};
// controllers/userController.js (updated)
const userService = require('../services/userService');
module.exports = {
getAllUsers: async (req, res, next) => {
try {
const users = await userService.getAllUsers();
res.json(users);
} catch (error) {
next(error);
}
},
getUserById: async (req, res, next) => {
try {
const user = await userService.getUserById(req.params.id);
res.json(user);
} catch (error) {
if (error.message === 'User not found') {
res.status(404).send({ message: error.message });
} else {
next(error);
}
}
}
};
The service layer encapsulates business logic and makes controllers leaner.
5. The Repository Pattern
For applications with database access, the repository pattern provides an abstraction layer between your models and database operations.
Example: Repository Pattern
// repositories/userRepository.js
const db = require('../database'); // Your database connection
module.exports = {
findAll: async () => {
try {
return await db.query('SELECT * FROM users');
} catch (error) {
throw new Error(`Database error: ${error.message}`);
}
},
findById: async (id) => {
try {
const result = await db.query('SELECT * FROM users WHERE id = ?', [id]);
return result[0];
} catch (error) {
throw new Error(`Database error: ${error.message}`);
}
},
create: async (userData) => {
try {
const result = await db.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
[userData.name, userData.email]
);
return { id: result.insertId, ...userData };
} catch (error) {
throw new Error(`Database error: ${error.message}`);
}
}
};
This pattern isolates database access and makes it easier to change the underlying database technology if needed.
6. The Factory Pattern
The factory pattern can be useful for creating objects or configurations in Express.
Example: Router Factory
// routerFactory.js
const express = require('express');
function createCrudRouter(controller) {
const router = express.Router();
router.get('/', controller.getAll);
router.get('/:id', controller.getById);
router.post('/', controller.create);
router.put('/:id', controller.update);
router.delete('/:id', controller.delete);
return router;
}
module.exports = { createCrudRouter };
// Using the factory
const userController = require('./controllers/userController');
const productController = require('./controllers/productController');
const { createCrudRouter } = require('./routerFactory');
app.use('/users', createCrudRouter(userController));
app.use('/products', createCrudRouter(productController));
This pattern reduces duplication by generating routers with standard CRUD operations.
7. The Middleware Chain Pattern
Express middleware can be chained to process requests in a specific order.
Example: Authentication and Authorization Chain
// Middleware chain for protected routes
const authenticateUser = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: 'Authentication required' });
}
try {
const decoded = verifyToken(token); // Hypothetical function
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ message: 'Invalid token' });
}
};
const checkAdminRole = (req, res, next) => {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ message: 'Admin access required' });
}
next();
};
// Using the middleware chain for a protected route
app.get('/admin/dashboard',
authenticateUser, // First middleware in the chain
checkAdminRole, // Second middleware in the chain
(req, res) => {
res.json({ message: 'Welcome to admin dashboard', user: req.user });
}
);
This pattern shows how to build secure routes with multiple validation layers.
Summary
Express.js design patterns help structure your applications in maintainable and scalable ways. We've covered:
- The Middleware Pattern: The core of Express functionality
- The Router Pattern: For modular route handling
- The MVC Pattern: Separation of concerns with Model-View-Controller
- The Service Layer Pattern: Business logic abstraction
- The Repository Pattern: Database access abstraction
- The Factory Pattern: Creating objects or configurations
- The Middleware Chain Pattern: Processing requests through multiple steps
As your Express applications grow in complexity, incorporating these patterns will help you maintain clean code architecture and make your projects easier to extend and maintain.
Additional Resources and Exercises
Resources
Exercises
-
Basic Middleware: Create a middleware that logs the request method, URL, and time and apply it to an Express application.
-
Router Implementation: Create a modular Express application with separate route files for users, products, and orders.
-
MVC Pattern: Convert a simple Express app to follow the MVC pattern with appropriate folder structure.
-
Service Layer: Add a service layer to your MVC application to handle complex business logic.
-
Full Stack Project: Build a small project implementing multiple patterns from this guide (e.g., a simple blog or todo app).
By practicing these patterns, you'll develop a deeper understanding of Express.js architecture and be better prepared to build robust web applications.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)