SOLID principles in JavaScript: from theory to practice on Node.js
SOLID is a set of five fundamental principles of object-oriented design formulated by Robert Martin (Uncle Bob). These principles help create flexible, maintainable, and scalable code. Although many associate SOLID with classical OOP languages (Java, C#), they are perfectly applicable in JavaScript, especially when developing on Node.js. In this article, we will break down each principle with specific examples.
Single Responsibility Principle (SRP)
SRP states: "A class (or module) should have only one reason to change". This means that each module is responsible for one specific task.
Bad example (SRP violation):
class UserService { createUser(data) { /* user creation logic */ } sendWelcomeEmail(email) { /* sending email */ } logActivity(action) { /* logging */ }}Here, one class is responsible for user management, sending emails, and logging. If the log format changes, you would have to modify UserService — even though its main task hasn't changed.
Good example (SRP compliance):
class UserRepository { createUser(data) { /* only database work */ }}
class EmailService { sendWelcomeEmail(email) { /* only sending */ }}
class Logger { logActivity(action) { /* only logging */ }}
class UserService { constructor(userRepo, emailService, logger) { this.userRepo = userRepo; this.emailService = emailService; this.logger = logger; } createUser(data) { const user = this.userRepo.createUser(data); this.emailService.sendWelcomeEmail(user.email); this.logger.logActivity('User created'); return user; }}Now each class is responsible for its own area of responsibility. By combining them in UserService, we gain flexibility and isolation of changes.
Open/Closed Principle (OCP)
OCP states: "Software entities should be open for extension, but closed for modification". That is, we should be able to add new functionality without changing existing code.
Bad example (OCP violation):
class PaymentProcessor { processPayment(type, amount) { if (type === 'card') { /* card processing */ } else if (type === 'paypal') { /* PayPal processing */ } // When adding a new type — you need to change this method }}Good example (OCP compliance):
class PaymentStrategy { pay(amount) { throw new Error('Must be implemented'); }}
class CardPayment extends PaymentStrategy { pay(amount) { console.log(`Paid ${amount} via Card`); }}
class PayPalPayment extends PaymentStrategy { pay(amount) { console.log(`Paid ${amount} via PayPal`); }}
class PaymentProcessor { constructor(strategy) { this.strategy = strategy; } processPayment(amount) { this.strategy.pay(amount); }}
// Usageconst cardPayment = new PaymentProcessor(new CardPayment());cardPayment.processPayment(100);
const paypalPayment = new PaymentProcessor(new PayPalPayment());paypalPayment.processPayment(200);Now to add a new payment method, you just need to create a new class inheriting from PaymentStrategy and pass it to PaymentProcessor. The original code does not change.
Liskov Substitution Principle (LSP)
LSP states: "Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program". In other words, if you have a base