SOLID Principles in JavaScript: Theory and Practice for Node.js

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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

Blogs

Book Recommendations