Published on

Applying SOLID Principles with Node.js and TypeScript: A Practical Guide

Authors
  • avatar
    Name
    Jonas de Oliveira
    Twitter

In modern software development, writing clean, modular, and maintainable code is essential for building scalable and robust applications. The SOLID principles provide a set of guidelines that help achieve these goals by promoting a well-structured architecture and making your code easier to evolve. In this article, we will explore how to apply the SOLID principles in a Node.js project using TypeScript. You will see practical examples with commented code that demonstrate how these concepts can transform the way you develop applications.

What Are the SOLID Principles?

SOLID is an acronym representing five essential principles for object-oriented software design:

  • S – Single Responsibility Principle (SRP): A class should have only one reason to change, meaning it should have only one responsibility.
  • O – Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
  • L – Liskov Substitution Principle (LSP): Objects of a derived class should be able to replace objects of the base class without altering the correctness of the program.
  • I – Interface Segregation Principle (ISP): Many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to depend on interfaces they do not use.
  • D – Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules; both should depend on abstractions.

Why Apply SOLID Principles with Node.js and TypeScript?

Applying the SOLID principles in your Node.js projects using TypeScript brings several benefits:

  • Easier Maintenance: Organized, modular code with clearly defined responsibilities simplifies maintenance and system evolution.
  • Scalability: Flexible structures allow your application to grow without compromising code quality.
  • Safety and Reliability: TypeScript’s static typing, combined with a SOLID design, helps catch runtime errors early and improves application robustness.
  • Market Relevance: Demonstrating mastery of SOLID principles and modern technologies captures the attention of recruiters and companies looking for professionals committed to best practices.

Practical Examples of SOLID Principles with Node.js and TypeScript

1. Single Responsibility Principle (SRP)

A class should have only a single responsibility. Below, we separate logging from message formatting to ensure each class has a distinct responsibility.

// src/logger/Logger.ts

/**
 * Logger class – responsible only for logging messages.
 */
export class Logger {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
}
// src/logger/MessageFormatter.ts

/**
 * MessageFormatter class – responsible only for formatting messages.
 */
export class MessageFormatter {
  format(message: string): string {
    const timestamp = new Date().toISOString();
    return `${timestamp} - ${message}`;
  }
}

Usage Example:

import { Logger } from './logger/Logger';
import { MessageFormatter } from './logger/MessageFormatter';

const logger = new Logger();
const formatter = new MessageFormatter();

const formattedMessage = formatter.format('Application started');
logger.log(formattedMessage);

2. Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification. Here, we define an interface for discount strategies and extend it through different implementations.

// src/discount/DiscountStrategy.ts

/**
 * Interface defining the discount strategy.
 */
export interface DiscountStrategy {
  applyDiscount(amount: number): number;
}
// src/discount/PercentageDiscount.ts
import { DiscountStrategy } from './DiscountStrategy';

/**
 * PercentageDiscount strategy – applies a percentage discount.
 */
export class PercentageDiscount implements DiscountStrategy {
  constructor(private discountPercent: number) {}

  applyDiscount(amount: number): number {
    return amount - (amount * this.discountPercent) / 100;
  }
}

Usage Example:

import { PercentageDiscount } from './discount/PercentageDiscount';

const discount = new PercentageDiscount(10);
console.log('10% Discount:', discount.applyDiscount(200)); // Output: 180

3. Liskov Substitution Principle (LSP)

Subclasses should be replaceable for their base classes without altering the correct behavior of the program.

// src/shapes/Shape.ts

/**
 * Base abstract class Shape for geometric shapes.
 */
export abstract class Shape {
  abstract area(): number;
}
// src/shapes/Rectangle.ts
import { Shape } from './Shape';

/**
 * Rectangle class extending Shape and implementing area calculation.
 */
export class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super();
  }

  area(): number {
    return this.width * this.height;
  }
}

Usage Example:

const rectangle = new Rectangle(5, 10);
console.log('Rectangle Area:', rectangle.area()); // Expected behavior

4. Interface Segregation Principle (ISP)

It's better to have many specific interfaces than one general-purpose interface.

// src/notifications/EmailNotifier.ts

/**
 * Interface for email notifications.
 */
export interface EmailNotifier {
  sendEmail(recipient: string, message: string): void;
}
// src/notifications/PushNotifier.ts

/**
 * Interface for push notifications.
 */
export interface PushNotifier {
  sendPush(recipientId: string, message: string): void;
}

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules; both should depend on abstractions.

// src/payment/PaymentProcessor.ts

/**
 * Interface defining the abstraction for payment processors.
 */
export interface PaymentProcessor {
  process(amount: number): boolean;
}
// src/payment/StripeProcessor.ts
import { PaymentProcessor } from './PaymentProcessor';

/**
 * StripeProcessor implementation of PaymentProcessor.
 */
export class StripeProcessor implements PaymentProcessor {
  process(amount: number): boolean {
    console.log(`Processing payment of $${amount} through Stripe.`);
    return true;
  }
}

Final Thoughts

Applying the SOLID principles in a Node.js project using TypeScript not only makes your code cleaner and more modular but also enhances the scalability and maintainability of your system. The practical examples above illustrate how each principle contributes to a robust and flexible architecture.