SOLID Principles in JavaScript: A Practical Guide
I'll be honest - when I first heard about SOLID principles, I thought they were just fancy OOP stuff that didn't apply to JavaScript. But after writing some messy code and having to refactor it later, I realized these principles actually help, even in JS.
What is SOLID?
SOLID is an acronym for five design principles:
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
Let me break them down in a way that makes sense for JavaScript.
Single Responsibility Principle
A class or function should have one reason to change. In JavaScript, this usually means: one function, one job.
Bad example:
function processUser(user) {
// Validate
if (!user.email) throw new Error('Invalid email')
// Save to database
db.users.save(user)
// Send email
emailService.sendWelcome(user.email)
// Log
logger.log('User created', user.id)
}
Better:
function validateUser(user) {
if (!user.email) throw new Error('Invalid email')
}
function saveUser(user) {
return db.users.save(user)
}
function sendWelcomeEmail(email) {
emailService.sendWelcome(email)
}
// Then compose them
const user = validateUser(rawUser)
const saved = saveUser(user)
sendWelcomeEmail(saved.email)
logger.log('User created', saved.id)
Each function does one thing. Easier to test, easier to change.
Open/Closed Principle
Open for extension, closed for modification. In JS, this often means using composition or higher-order functions.
// Instead of modifying this function
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0)
}
// Make it extensible
function calculateTotal(items, discount = 0) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0)
return subtotal * (1 - discount)
}
// Or use a strategy pattern
function calculateTotal(items, calculator) {
return calculator(items)
}
const regularCalculator = (items) => items.reduce((sum, item) => sum + item.price, 0)
const discountCalculator = (items) => regularCalculator(items) * 0.9
Liskov Substitution Principle
Subtypes should be substitutable for their base types. In JavaScript, this is about making sure your functions work as expected.
If you have a function that expects an array, it should work with any array-like object:
function processItems(items) {
// Should work with arrays, NodeLists, etc.
return Array.from(items).map(processItem)
}
Interface Segregation Principle
Don't force clients to depend on methods they don't use. In JavaScript, this means keeping your objects focused.
Bad:
class User {
getName() { }
getEmail() { }
getAddress() { }
getPaymentInfo() { }
getPreferences() { }
}
// But sometimes you only need name and email
function displayUser(user) {
// Forced to have access to everything
return `${user.getName()} - ${user.getEmail()}`
}
Better:
// Create smaller, focused interfaces
function displayUser(user) {
return `${user.name} - ${user.email}`
}
// Only pass what's needed
displayUser({ name: user.name, email: user.email })
Dependency Inversion Principle
Depend on abstractions, not concretions. In JavaScript, this often means using dependency injection.
Bad:
class UserService {
constructor() {
this.db = new MySQLDatabase() // Tightly coupled
}
}
Better:
class UserService {
constructor(db) {
this.db = db // Depends on abstraction
}
}
// Now you can pass any database
const userService = new UserService(new MySQLDatabase())
const userService2 = new UserService(new MongoDB())
Real-World Example
Here's how I refactored some code using these principles:
Before:
async function handleUserSignup(data) {
const user = await db.users.create(data)
await email.send(user.email, 'Welcome!')
await analytics.track('signup', user.id)
return user
}
After:
// Single responsibility
async function createUser(data) {
return db.users.create(data)
}
async function sendWelcomeEmail(email) {
return email.send(email, 'Welcome!')
}
async function trackSignup(userId) {
return analytics.track('signup', userId)
}
// Compose them
async function handleUserSignup(data) {
const user = await createUser(data)
await Promise.all([
sendWelcomeEmail(user.email),
trackSignup(user.id)
])
return user
}
My Take
SOLID principles aren't rules you must follow. They're guidelines that help you write maintainable code. Sometimes breaking them is fine if it makes sense for your situation.
The key is understanding why they exist - to make code easier to change, test, and understand. If your code is already doing that, you're probably following the spirit of SOLID even if not the letter.
Start with Single Responsibility - it's the easiest to apply and makes the biggest difference. The rest will follow naturally as your codebase grows.