4 Design Patterns in Node.js You Should Know

Design patterns are reusable solutions to common problems that arise in software development. They provide a way to structure the code and make it easier to maintain and scale.

In this article, we’ll discuss some of the most commonly used design patterns in Node.js and how they can be used to build efficient and scalable applications.

1. Factory pattern

The Factory pattern is a creational design pattern that provides a simple way to create objects without having to specify the exact class of object that will be created.

This is particularly useful in Node.js, where the same object may be created in multiple places throughout the application.

With the Factory pattern, a single object acts as a factory and is responsible for creating new objects. This object can be configured to return different types of objects based on the inputs it receives.

This makes it easier to change the object creation process in the future, without having to modify the code in multiple places.

Factory Design

function vehicleFactory(type) {
  if (type === "car") {
    return new Car();
  } else if (type === "truck") {
    return new Truck();
  } else {
    throw new Error(`Unsupported vehicle type: ${type}`);
  }
}

class Car {
  drive() {
    console.log("Driving a car");
  }
}

class Truck {
  drive() {
    console.log("Driving a truck");
  }
}

const car = vehicleFactory("car");
car.drive(); // Output: Driving a car

const truck = vehicleFactory("truck");
truck.drive(); // Output: Driving a truck

In the example, the vehicleFactory function takes a type parameter, which determines the type of vehicle to create. Based on the value of type, the function returns an instance of either the Car class or the Truck class. The factory function acts as a central point of control for creating objects, making it easy to add or remove types of vehicles in the future.

2. Singleton pattern

The Singleton pattern is a creational design pattern that ensures that a class has only one instance, while providing a global point of access to this instance for the rest of the application.

This pattern is useful in Node.js because it allows for a shared resource to be used throughout the application, without the risk of multiple instances being created.

For example, a database connection can be implemented as a singleton. This ensures that only one connection is established, even if the database is accessed from multiple places in the application.

This helps to reduce the overhead of establishing multiple connections, as well as providing a central point of control for the database connection.

Singleton Class Example

const databaseSingleton = (() => {
  let instance;

  const createInstance = () => {
    const database = new Database();
    return database;
  };

  return {
    getInstance: () => {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

class Database {
  constructor() {
    console.log("Creating a new database connection");
  }
}

const db1 = databaseSingleton.getInstance();
const db2 = databaseSingleton.getInstance();

console.log(db1 === db2); // Output: true

In the code above, the databaseSingleton variable is a closure that implements the singleton pattern.

The closure returns an object with a getInstance method, which returns the single instance of the Database class. The first time the getInstance method is called, it creates a new instance of the Database class and stores it in the instance variable.

Subsequent calls to getInstance simply return the existing instance. This ensures that there is only one instance of the Database class in the entire application, and that it can be easily accessed from any part of the code.

3. Observer pattern

The Observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects, such that when one object changes state, all its dependents are notified and updated automatically. This pattern is useful in Node.js for implementing real-time updates, such as push notifications.

For example, a stock price update can be implemented using the Observer pattern. Whenever the stock price changes, the observer objects are notified and updated automatically, ensuring that the user always has the latest information.

Observer Class Example

class EventObserver {
  constructor() {
    this.observers = [];
  }

  subscribe(fn) {
    this.observers.push(fn);
  }

  unsubscribe(fn) {
    this.observers = this.observers.filter((subscriber) => subscriber !== fn);
  }

  broadcast(data) {
    this.observers.forEach((subscriber) => subscriber(data));
  }
}

const observer = new EventObserver();

const observerA = (data) => console.log(`Observer A: ${data}`);
const observerB = (data) => console.log(`Observer B: ${data}`);

observer.subscribe(observerA);
observer.subscribe(observerB);

observer.broadcast("Hello, Observers!");

In this example, the EventObserver class acts as the subject, and the observerA and observerB functions act as the observers. The subject maintains a list of subscribers and provides methods to add or remove subscribers, as well as to broadcast events to all subscribers. When the broadcast method is called, it iterates through the list of subscribers and calls each one with the data provided.

4. Command pattern

The Command pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing for deferred execution of requests, as well as the ability to queue or log requests.

This pattern is useful for implementing undo-redo functionality, as well as for improving the performance of an application by allowing requests to be executed in the background.

For example, a user’s request to save changes to a document can be implemented as a command object.

The command object can be executed immediately, or it can be deferred and executed at a later time, or even logged for auditing purposes. This flexibility allows for a more streamlined and efficient handling of user requests.

Example of the Command Pattern

// Command interface
class Command {
  execute() {}
}

// Concrete Command 1
class ConcreteCommand1 extends Command {
  constructor(receiver) {
    super();
    this.receiver = receiver;
  }

  execute() {
    console.log("ConcreteCommand1 executing...");
    this.receiver.action1();
  }
}

// Concrete Command 2
class ConcreteCommand2 extends Command {
  constructor(receiver) {
    super();
    this.receiver = receiver;
  }

  execute() {
    console.log("ConcreteCommand2 executing...");
    this.receiver.action2();
  }
}

// Receiver
class Receiver {
  action1() {
    console.log("Receiver performing action 1...");
  }

  action2() {
    console.log("Receiver performing action 2...");
  }
}

// Invoker
class Invoker {
  constructor() {
    this.commands = [];
  }

  setCommand(command) {
    this.commands.push(command);
  }

  executeCommands() {
    this.commands.forEach((command) => command.execute());
  }
}

// Client
class Client {
  run() {
    const receiver = new Receiver();
    const command1 = new ConcreteCommand1(receiver);
    const command2 = new ConcreteCommand2(receiver);

    const invoker = new Invoker();
    invoker.setCommand(command1);
    invoker.setCommand(command2);
    invoker.executeCommands();
  }
}

const client = new Client();
client.run();

// Output:
// ConcreteCommand1 executing...
// Receiver performing action 1...
// ConcreteCommand2 executing...
// Receiver performing action 2...

In this example, we have a Client class that sets up and executes the commands. The Invoker class keeps a list of commands and executes them when requested. The Receiver class performs the actual actions that the commands represent.

The ConcreteCommand1 and ConcreteCommand2 classes are concrete implementations of the Command interface, each representing a specific action that the Receiver can perform.

Conclusion

Design patterns are an essential tool for building scalable and efficient applications in Node.js. By following these patterns, developers can structure their code in a way that makes it easier to maintain and scale as the application grows.

In this article, we’ve discussed five of the most commonly used design patterns in Node.js: the Factory pattern, the Singleton pattern, the Observer pattern, and the Command pattern. By using these patterns, developers can build robust and efficient applications that meet the demands of their users.

Like this type of content? Drop a reaction below and feel free to post any questions or comments!

Special Offer

If you’ve made it to the bottom of this article, congrats. We’d like to offer you our ebook at 70% off. Get the ebook for $3 by following this link.

Note: We do know that some of our readers are in different economies. If you cannot afford the book, send us an email for a free copy: contact (at) javascripttoday.com. Please include your country in the subject line.

comments powered by Disqus

Related Posts

The Great JavaScript Debate: To Semicolon or Not?

Since I’ve started learning this language, JavaScript has undergone some heavy changes. Most notably, it seems to be the norm to not use semicolons anymore.

Read more

Hacktoberfest 2024: Get a Free JavaScript Today Sticker

October is here, and that means one thing in the tech world—Hacktoberfest! This annual event, powered by DigitalOcean, Cloudflare, Quira, and other sponsors, encourages developers of all skill levels to contribute to open-source projects.

Read more

Creating a Real Time Chat Application with React, Node, and TailwindCSS

In this tutorial, we will show you how to build a real-time chat application using React and Vite,as well as a simple Node backend.

Read more