Design Patterns in Node.js

Ben Mishali
5 min readFeb 25, 2023

--

Introduction

Design patterns are reusable solutions to common programming problems that have been identified and documented over time by experienced programmers. They provide a way to standardize code design and improve software development. Node.js, being a popular platform for building scalable and high-performance applications, also follows design patterns to solve common problems. In this article, we will discuss the importance of design patterns in Node.js and provide some code examples.

Why we need design patterns in Node.js?

Design patterns provide a structured approach to solving recurring problems in software development. In Node.js, these patterns help developers to write better, maintainable, and scalable code. Design patterns in Node.js also help in improving code quality, reducing development time, and reducing errors. They also provide a common vocabulary for developers to communicate with each other.

Code Examples

Singleton Pattern

The Singleton pattern is used to ensure that only one instance of a class is created, and that instance is available throughout the application. This pattern is used in Node.js to ensure that there is only one instance of a module.

class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
}

// Your code here
}

module.exports = Singleton;

Factory Pattern

The Factory pattern is used to create objects without exposing the creation logic to the client. In Node.js, this pattern is used to create instances of different classes based on the input provided.

class Car {
constructor(name) {
this.name = name;
}
drive() {
console.log(`Driving ${this.name}`);
}
}

class CarFactory {
static create(name) {
return new Car(name);
}
}

const car1 = CarFactory.create("BMW");
const car2 = CarFactory.create("Audi");

car1.drive(); // Driving BMW
car2.drive(); // Driving Audi

Observer Pattern

The Observer pattern is used to maintain a list of dependents that need to be notified when a change happens to the object being observed. In Node.js, this pattern is used to manage events and callbacks.

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

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

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

notify(data) {
this.observers.forEach(observer => observer(data));
}
}

const eventObserver = new EventObserver();

eventObserver.subscribe(data => console.log(`Subscribed to ${data}`));
eventObserver.notify("some data");

Dependency Injection pattern

In this example, we’re defining a UserService class that depends on a database object. We're injecting the database object into the UserService constructor, which allows us to use different database implementations without modifying the UserService class.

// File: userService.js

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

getUser(id) {
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}

module.exports = UserService;

Promise pattern

In this example, we’re using the fs.promises module to read a file asynchronously. The readFile function returns a promise that resolves with the contents of the file or rejects with an error. We're using the then method to log the contents of the file if the promise is resolved, and the catch method to log the error if the promise is rejected.

// File: fileReader.js

const fs = require('fs').promises;

function readFile(filePath) {
return fs.readFile(filePath, 'utf8');
}

readFile('example.txt')
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});

built-in modules

Node.js itself doesn’t use any specific design patterns in its features by default, but it provides built-in modules that follow common design patterns. Some of the commonly used design patterns in Node.js are:

Module pattern

Node.js uses the module pattern by default to organize code into reusable and maintainable modules. In Node.js, each file is treated as a module, and developers can export or import code between files using the “require” and “module.exports” statements.

Event-driven pattern

Node.js uses an event-driven pattern to handle I/O operations, such as reading and writing data to files or network sockets. The event-driven pattern is based on the Observer pattern and allows developers to create event emitters that can notify listeners when certain events occur.

Singleton pattern

Node.js uses the Singleton pattern to ensure that certain objects are only instantiated once, such as the process object, which represents the current Node.js process.

Factory pattern

Node.js uses the Factory pattern in its built-in modules, such as the “http” module, which provides a factory method for creating HTTP servers.

Callback pattern

Node.js uses the callback pattern to handle asynchronous operations, such as reading and writing files or making network requests. The callback pattern is based on the Observer pattern and allows developers to pass functions as arguments to be executed when an operation is complete.

Other Modules

Middleware pattern

Middleware is a design pattern commonly used in Node.js frameworks such as Express.js. Middleware functions are functions that are executed in a pipeline, where each function can modify the request or response objects before passing them on to the next function. Middleware can be used for tasks such as authentication, logging, error handling, and more.

Dependency Injection pattern

The Dependency Injection (DI) pattern is a design pattern used to manage dependencies between objects. In Node.js, DI can be used to inject dependencies into modules and make them more modular and reusable. DI can be implemented using techniques such as constructor injection, property injection, or method injection.

Promise pattern

The Promise pattern is a design pattern used to handle asynchronous operations in a more structured and synchronous-like way. Promises are objects that represent the eventual completion or failure of an asynchronous operation, and they allow developers to write more readable and maintainable code by chaining asynchronous operations together.

Conclusion

Design patterns provide a structured approach to solving common programming problems in Node.js. They help developers to write better, maintainable, and scalable code. Design patterns also provide a common vocabulary for developers to communicate with each other. These patterns, and others, are essential to writing high-quality code in Node.js.

Visit me at ben.dev.io

--

--

Ben Mishali

𝔹𝕖𝕟 | 𝕊𝕠𝕗𝕥𝕨𝕒𝕣𝕖 𝔼𝕟𝕘𝕚𝕟𝕖𝕖𝕣 🕵‍♂️ Technologies Explorer 👨🏻‍💻 Web & Native developer 🤓 Sharing my knowledge and experiences