Hernando Abella
Chapter 1SOLIDJavaScriptDesign Patterns

Applying SOLID Principles in Modern JavaScript Applications

Learn how to write maintainable, scalable, and testable JavaScript code using the five SOLID principles — essential for modern Node.js and frontend development.

📖 20 min read🧑‍💻 Hernando Abella📘 Node.js Design Patterns
StackJavaScriptTypeScriptNode.jsReactNestJS

Modern JavaScript applications have grown far beyond simple scripts. As applications grow, maintaining clean, scalable, and testable code becomes increasingly important.

The SOLID principles, originally introduced by Robert C. Martin (Uncle Bob), provide a foundation for creating maintainable software architectures. Although these principles originated in object-oriented programming, they remain highly relevant in modern JavaScript development.


What Is SOLID?

SOLID is an acronym representing five design principles:

SRP
Single Responsibility

A class should have only one reason to change.

OCP
Open/Closed

Open for extension, closed for modification.

LSP
Liskov Substitution

Subtypes must be substitutable for their base types.

ISP
Interface Segregation

Clients should not depend on methods they don't use.

DIP
Dependency Inversion

Depend on abstractions, not concretions.


Single Responsibility Principle (SRP)

Definition: A class, module, or function should have only one reason to change.

Bad Example

javascript · bad-example.js
class UserService {
  async createUser(userData) {
    // Validate data
    if (!userData.email) {
      throw new Error("Email required");
    }

    // Save to database
    await database.users.insert(userData);

    // Send email
    await emailService.sendWelcomeEmail(userData.email);

    // Log action
    console.log("User created");
  }
}

Better Example

javascript · good-example.js
class UserValidator {
  validate(user) {
    if (!user.email) {
      throw new Error("Email required");
    }
  }
}

class UserRepository {
  async save(user) {
    return database.users.insert(user);
  }
}

class NotificationService {
  async sendWelcomeEmail(email) {
    return emailService.sendWelcomeEmail(email);
  }
}

class UserService {
  constructor(validator, repository, notifier) {
    this.validator = validator;
    this.repository = repository;
    this.notifier = notifier;
  }

  async createUser(user) {
    this.validator.validate(user);
    await this.repository.save(user);
    await this.notifier.sendWelcomeEmail(user.email);
  }
}

✓ Benefits: Easier testing, improved readability, reduced coupling, better maintainability


Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.

Bad Example

javascript · bad-example.js
function calculateDiscount(customerType, amount) {
  if (customerType === "regular") {
    return amount * 0.05;
  }
  if (customerType === "premium") {
    return amount * 0.10;
  }
  if (customerType === "vip") {
    return amount * 0.20;
  }
}

Better Example

javascript · good-example.js
class DiscountStrategy {
  calculate(amount) {
    return 0;
  }
}

class RegularDiscount extends DiscountStrategy {
  calculate(amount) {
    return amount * 0.05;
  }
}

class PremiumDiscount extends DiscountStrategy {
  calculate(amount) {
    return amount * 0.10;
  }
}

class VipDiscount extends DiscountStrategy {
  calculate(amount) {
    return amount * 0.20;
  }
}

// Usage
const strategy = new VipDiscount();
const discount = strategy.calculate(1000);

Liskov Substitution Principle (LSP)

Definition: Subtypes must be replaceable for their base types without altering application behavior.

Bad Example

javascript · bad-example.js
class Bird {
  fly() {
    console.log("Flying");
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("Penguins cannot fly");
  }
}

Better Example

javascript · good-example.js
class Bird {}

class FlyingBird extends Bird {
  fly() {
    console.log("Flying");
  }
}

class Eagle extends FlyingBird {}

class Penguin extends Bird {}

// Now Penguin can be used anywhere Bird is expected

Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on methods they do not use.

javascript · good-example.js
class Workable {
  work() {}
}

class Eatable {
  eat() {}
}

class Developer extends Workable {
  work() {
    console.log("Writing code");
  }
}

class Human extends Eatable {
  eat() {
    console.log("Eating");
  }
}

// Each class depends only on behaviors it actually needs

Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Bad Example

javascript · bad-example.js
class MySQLDatabase {
  save(data) {
    console.log("Saving to MySQL");
  }
}

class UserService {
  constructor() {
    this.database = new MySQLDatabase();
  }

  createUser(user) {
    this.database.save(user);
  }
}

Better Example

javascript · good-example.js
class UserService {
  constructor(database) {
    this.database = database;
  }

  createUser(user) {
    this.database.save(user);
  }
}

class MySQLDatabase {
  save(data) {
    console.log("MySQL save");
  }
}

class MongoDatabase {
  save(data) {
    console.log("MongoDB save");
  }
}

// Usage - easy to switch implementations
const database = new MongoDatabase();
const userService = new UserService(database);

✓ Benefits: Easier testing, better flexibility, improved maintainability, simplified dependency injection


SOLID in Modern JavaScript Frameworks

React

  • Components follow SRP
  • Hooks separate concerns
  • Context promotes dependency inversion

Node.js

  • Services and repositories support SRP
  • Middleware encourages OCP
  • Dependency injection supports DIP

NestJS

  • Modules isolate responsibilities
  • Providers use dependency injection
  • Interfaces encourage loose coupling

TypeScript

  • Interfaces make abstractions explicit
  • Strong typing improves maintainability
  • Better refactoring support

Common Mistakes When Applying SOLID

  • ⚠️ Creating unnecessary abstractions
  • ⚠️ Building deep inheritance trees
  • ⚠️ Overusing interfaces
  • ⚠️ Introducing complexity too early

SOLID should solve real design problems rather than serve as a rigid set of rules.


Practical Example: Refactoring a Node.js Service

Before SOLID

javascript · before.js
class OrderService {
  async createOrder(order) {
    validate(order);
    saveToDatabase(order);
    sendEmail(order);
    processPayment(order);
  }
}

After Applying SOLID

javascript · after.js
class OrderService {
  constructor(
    validator,
    repository,
    paymentProcessor,
    notificationService
  ) {
    this.validator = validator;
    this.repository = repository;
    this.paymentProcessor = paymentProcessor;
    this.notificationService = notificationService;
  }

  async createOrder(order) {
    this.validator.validate(order);
    await this.paymentProcessor.process(order);
    await this.repository.save(order);
    await this.notificationService.notify(order);
  }
}

✓ The result is a system that is easier to test, extend, and maintain.


Conclusion

SOLID principles remain highly relevant in modern JavaScript development. Whether you're building React applications, Node.js APIs, microservices, or enterprise systems, these principles provide a practical framework for writing maintainable software.

The key goal of SOLID is not to increase complexity but to create code that adapts gracefully to change. By applying these principles thoughtfully, developers can build JavaScript applications that remain scalable and manageable as requirements evolve.

Mastering SOLID is one of the most valuable investments a JavaScript developer can make for long-term code quality and architectural success.


📘 From the Book

Node.js Design Patterns

Master SOLID principles, design patterns, and best practices for building scalable Node.js applications. Includes real-world examples and production-ready code.

🎯 SOLID Principles🏗️ Design Patterns⚡ Performance🔧 Best Practices
Get it on Amazon →
Node.js Design Patterns book cover