How to Implement SOLID Principles in Express.js and Next.js

Table of Contents
Introduction to SOLID Principles in Modern Web Development
In today's fast-paced web development landscape, building applications that are maintainable, scalable, and easy to modify is crucial. This is where SOLID principles come into play - a set of five design principles that help developers create robust software architectures.
Originally coined by Robert C. Martin (Uncle Bob), SOLID principles are particularly valuable when working with:
- Express.js - For building backend APIs and services
- Next.js - For developing React-based frontend applications
What Does SOLID Stand For?
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
By applying these principles to your Express.js and Next.js projects, you'll achieve:
- ✅ Cleaner, more organized code
- ✅ Easier maintenance and updates
- ✅ Better testability
- ✅ Reduced coupling between components
- ✅ More flexible architecture
1. Single Responsibility Principle (SRP)
A module should have only one reason to change.
Express.js Implementation
❌ Violating SRP (Mixed Concerns)
// routes/users.js - Bad example
router.post('/', (req, res) => {
// Validation
if (!req.body.email) {
return res.status(400).json({ error: 'Email is required' });
}
// Business logic
const user = createUser(req.body);
// Database operation
UserModel.save(user, (err) => {
if (err) return res.status(500).json({ error: 'DB error' });
res.status(201).json(user);
});
});
Problems: Route handles validation, business logic, and database operations.
✅ Proper SRP Implementation
// controllers/userController.js
const userService = require('../services/userService');
exports.createUser = async (req, res) => {
try {
const user = await userService.createUser(req.body);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// services/userService.js
const UserModel = require('../models/User');
exports.createUser = async (userData) => {
if (!userData.email) throw new Error('Email is required');
return await UserModel.create(userData);
};
Benefits: Clear separation of concerns, easier testing, and maintenance.
Next.js Implementation
❌ Mixed API Route
// pages/api/users.js - Bad example
export default (req, res) => {
if (req.method === 'POST') {
// Validation
if (!req.body.name) return res.status(400).json({ error: 'Name required' });
// Business logic
const newUser = { id: Date.now(), ...req.body };
// DB operation
db.users.push(newUser);
res.status(201).json(newUser);
}
};
✅ SRP-Compliant Next.js API
// lib/services/userService.js
export const createUser = (userData) => {
if (!userData.name) throw new Error('Name required');
return { id: Date.now(), ...userData };
};
// pages/api/users.js
import { createUser } from '../../lib/services/userService';
export default (req, res) => {
if (req.method === 'POST') {
try {
const user = createUser(req.body);
// DB operation would be separate
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
};
2. Open/Closed Principle (OCP)
Software should be open for extension but closed for modification.
Express.js Implementation
❌ Violating OCP
// Bad: Need to modify this function for new report types
function generateReport(type, data) {
if (type === 'pdf') {
// PDF generation logic
} else if (type === 'csv') {
// CSV generation logic
}
// Adding new report type requires modifying this function
}
✅ OCP-Compliant Solution
// strategies/reportStrategies.js
const reportStrategies = {
pdf: (data) => { /* PDF generation */ },
csv: (data) => { /* CSV generation */ },
excel: (data) => { /* Excel generation - added without modifying existing code */ }
};
// services/reportService.js
exports.generateReport = (type, data) => {
const strategy = reportStrategies[type];
if (!strategy) throw new Error('Invalid report type');
return strategy(data);
};
Next.js Implementation
❌ Hardcoded Component
function Button({ type }) {
if (type === 'primary') {
return <button className="bg-blue-500">Primary</button>;
} else if (type === 'secondary') {
return <button className="bg-gray-500">Secondary</button>;
}
// Adding new types requires modification
}
✅ Extensible Component
function Button({ variant = 'primary', children }) {
const variants = {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-500 text-black',
danger: 'bg-red-500 text-white'
// New variants can be added without modifying Button
};
return (
<button className={`px-4 py-2 rounded ${variants[variant]}`}>
{children}
</button>
);
}
3. Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types.
Express.js Implementation
❌ Violating LSP
class Database {
query(sql) { /* Execute any query */ }
}
class ReadOnlyDatabase extends Database {
query(sql) {
if (sql.startsWith('INSERT')) throw new Error('Read-only!');
super.query(sql);
}
}
// This breaks existing code expecting full DB functionality
const db = new ReadOnlyDatabase();
db.query('INSERT INTO users...'); // Throws error
✅ LSP-Compliant Solution
interface Database {
query(sql: string): Promise<any>;
}
class ReadWriteDatabase implements Database {
async query(sql) { /* Execute any query */ }
}
class ReadOnlyDatabase implements Database {
async query(sql) {
if (sql.startsWith('INSERT')) {
throw new Error('Read-only database');
}
return executeReadQuery(sql);
}
}
4. Interface Segregation Principle (ISP)
Clients shouldn't depend on interfaces they don't use.
Express.js Implementation
❌ Fat Middleware
// Bad: Forces all routes to use features they might not need
app.use((req, res, next) => {
logRequest(req); // Not all routes need logging
authenticate(req); // Not all routes need auth
rateLimit(req); // Not all routes need rate limiting
next();
});
✅ Segregated Middleware
// Separate middleware modules
const logRequest = require('./middleware/logging');
const authenticate = require('./middleware/auth');
const rateLimit = require('./middleware/rateLimit');
// Apply only what's needed
app.get('/public', (req, res) => res.send('Hello'));
app.post('/login', logRequest, authenticate, (req, res) => { /* ... */ });
app.post('/api', logRequest, rateLimit, authenticate, (req, res) => { /* ... */ });
5. Dependency Inversion Principle (DIP)
Depend on abstractions, not concretions.
Express.js Implementation
❌ Tight Coupling
// Bad: Directly depends on MongoDB
const MongoClient = require('mongodb');
const client = new MongoClient(process.env.DB_URI);
app.get('/users', async (req, res) => {
const users = await client.db().collection('users').find().toArray();
res.json(users);
});
✅ DIP-Compliant Solution
// interfaces/UserRepository.js
class UserRepository {
async findAll() {
throw new Error('Method not implemented');
}
}
// repositories/MongoUserRepository.js
class MongoUserRepository extends UserRepository {
constructor(client) {
super();
this.client = client;
}
async findAll() {
return this.client.db().collection('users').find().toArray();
}
}
// controllers/userController.js
class UserController {
constructor(repository) {
this.repository = repository;
}
async getUsers(req, res) {
const users = await this.repository.findAll();
res.json(users);
}
}
// app.js
const MongoClient = require('mongodb');
const client = new MongoClient(process.env.DB_URI);
const repository = new MongoUserRepository(client);
const userController = new UserController(repository);
app.get('/users', (req, res) => userController.getUsers(req, res));
Conclusion and Best Practices
Implementing SOLID principles in your Express.js and Next.js applications leads to:
- 🚀 More maintainable codebases
- 🧩 Better component reusability
- 🔧 Easier refactoring and updates
- 🧪 Improved testability
- ⚡ More scalable architecture
Key Takeaways
- SRP: Keep modules focused on single responsibilities
- OCP: Design systems that can be extended without modification
- LSP: Ensure subtypes can replace their parents without issues
- ISP: Create small, focused interfaces
- DIP: Depend on abstractions rather than concrete implementations
By consistently applying these principles, you'll build web applications that stand the test of time and are a joy to work with for your entire team.