DEV Community

Cover image for Five Design Patterns to know in Node.js
Jakub Andrzejewski
Jakub Andrzejewski

Posted on • Edited on

Five Design Patterns to know in Node.js

Hey there!

I recently went through multiple knowledge resources to learn more about popular design and architectural patterns in Node.js. My aim was mainly at the server (backend) side but as I was going through them, I was seeing a lot of similarities to the browser (frontend) frameworks. Some of them are even directly used in the frameworks for which I am even more happy because I was using them already without knowing it 😉

There are many (really many) design patterns that you could use, so in this article, I decided to choose 5 of them and explain them in more detail.

Enjoy!

🟢 What is a Design Pattern?

Design patterns are proven and battle-tested solutions to solve problems that we as developers encounter every day. These patterns help promote best practices and implement a structured approach to solving everyday issues while designing and developing software architecture. Software engineers can develop maintainable, secure, and stable systems by using these patterns.

Node.js due to its flexibility does not force you to stick to certain patterns but instead gives you the freedom of choosing just the ones needed for your task. That is why in my opinion it is so widely used today (and by the way thanks to JavaScript :D).

✅ Five Popular Design Patterns in Node.js

Below, you will see a list of 5 selected design patterns that I like.

Singleton

This pattern is all about classes that can have only one instance and provide global access to it. Modules can be cached and shared across the application in Node.js which will help improve the efficiency of resources. A common example of such a singleton pattern is a module for connecting with certain third-party services like databases, cache services, email providers, etc that is used extensively in the Nest.js framework. Let's take a look at the following example:

class Redis {
  constructor() {
    this.connection = null;
  }

  static getInstance() {
    if (!Redis.instance) {
      Redis.instance = new Redis(options);
    }

    return Redis.instance;
  }

  connect() {
    this.connection = 'Redis connected'
  }
}
Enter fullscreen mode Exit fullscreen mode

And then we can use it like the following:

const redisOne = Redis.getInstance();
const redisTwo = Redis.getInstance();

console.log(redisOne === redisTwo); // it will result to `true`

redisOne.connect();

console.log(redisOne.connection) // 'Redis connected'
console.log(redisTwo.connection) // 'Redis connected'
Enter fullscreen mode Exit fullscreen mode

This approach ensures that there is only one connection to Redis and prevents duplicating connections.

Factory

With this pattern, you can create new objects without specifying the class of object that will be created. Thanks to it we are abstracting object creation which can help improve code readability and reusability:

class Character {
  constructor(type, health) {
    this.type = type;
    this.health = health;
  }
}

class CharacterFactory {
  createCharacter(name) {
    switch(name) {
      case 'mage': 
        return new Character('Powerful Mage', 8);
      case 'warrior':
        return new Character('Courageous Warrior', 10);
      case 'rogue':
        return new Character('Sneaky Rogue', 9)
      default:
        return new Error('Unknown character');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And then we can use it like the following:

const characterFactory = new CharacterFactory();

const mage = characterFactory.createCharacter('mage');
const warrior = characterFactory.createCharacter('warrior');

console.log(mage.type) // Powerful Mage
console.log(warrior.type) // Courageous Warrior
Enter fullscreen mode Exit fullscreen mode

This approach allows consumers of this factory to use the factory code instead of using the Character class constructor directly.

Observer

This pattern works in a way that you will have an entity that manages the list of depending elements called observers and notifies them if the state changes. This pattern is used widely in the Vue.js framework and be implemented like this:

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

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

  unsubscribe(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }

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

class Observer {
  constructor(name) {
    this.name = name;
  }

  update(data) {
    console.log(`${this.name} received ${data}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

And you can use it like the following:

const topic = new Topic();

const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

topic.subscribe(observer1);
topic.subscribe(observer2);

topic.notify('Hello World');
// Observer 1 received Hello World 
// Observer 2 received Hello World

topic.unsubscribe(observer2);

topic.notify('Hello Again');
// Observer 1 received Hello Again
Enter fullscreen mode Exit fullscreen mode

It is a really useful pattern for event handling and asynchronous workflows that allows to update of multiple objects without coupling the publisher to the subscribers.

Decorator

This pattern is quite useful for extending the existing functionality with a new one without affecting the initial/original instances. It is used widely in the Nest.js framework thanks to the full support of TypeScript but in regular Node.js it can be used in following:

class Character {
  constructor() {
    this.endurance = 10;
  }

  getEndurance() {
    return this.endurance;
  }
}

class CharacterActions {
  constructor(character) {
    this.character = character;
  }

  attack() {
    this.character.endurance -= 2;
  }

  rest() {
    this.character.endurance += 1; 
  }
}
Enter fullscreen mode Exit fullscreen mode

And then it can be used like the following:

const character = new Character();

console.log(character.getEndurance()); // 10

const characterWithActions = new CharacterActions(character);

characterWithActions.attack(); // - 2
characterWithActions.rest(); // + 1

console.log(characterWithActions.character.getEndurance()); // 9
Enter fullscreen mode Exit fullscreen mode

By using this pattern we can easily extend already existing classes without affecting their core functionality.

Dependency Injection

In this pattern, classes or modules receive dependencies from external sources rather than registering them internally. This approach allows extracting certain reusable elements from your system for easier testing and maintenance. It is used quite extensively in the Nest.js framework. It can be implemented like following:

class UserService {
  constructor(databaseService, loggerService) {
    this.db = databaseService;
    this.logger = loggerService;
  }

  async getUser(userId) {
    const user = await this.db.findUserById(userId);
    this.logger.log(`Fetched user ${user.name}`);
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

And then, you can use it like following:

const databaseService = new Database();
const loggerService = new Logger();

const userService = new UserService(databaseService, loggerService);

userService.getUser(1);
Enter fullscreen mode Exit fullscreen mode

This approach allows you to extract elements of your system into intependent entities that can be injected when needed.

📖 Learn more

If you would like to learn more about Vue, Nuxt, JavaScript or other useful technologies, check VueSchool by clicking this link or by clicking the image below:

Vue School Link

It covers the most important concepts while building modern Vue or Nuxt applications that can help you in your daily work or side projects 😉

✅ Summary

Well done! You have just learned how certain design patterns work in Node.js and how to implement them.

Take care and see you next time!

And happy coding as always 🖥️

Top comments (21)

Collapse
 
doronguttman profile image
Doron Guttman

Trying to be positive here, but these examples leave a lot of room for improvement. I.e.

  1. Singleton: when using modules (which you should), there is no need to use a getInstance static method, when you can just export the instance by itself. E.g.
export const redis = new Redis(...);
export default redis;
Enter fullscreen mode Exit fullscreen mode
  1. Factory: instead of using a switch, you should be using a factory map. Much more extensible and (although micro) optimized. E.g.
const factory = FACTORIES[name];
if (!factory) {
  throw...
}
return factory();
Enter fullscreen mode Exit fullscreen mode
  1. Observer: use a Set instead of an array. And wrap notifications in try-catch so a single error won't impact following observers. E.g.
// Subscribe
this.observers.set(observer);
// Unsubscribe 
this.observers.delete(observer);
// Notify
for (const observer of this.observers) {
  try {
    observer.update(...)
  } catch (e) {
    // Log
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Decorator: use getters and private fields instead of java-style get methods. E.g.
get endurance() {
  return this.#endurance;
}
Enter fullscreen mode Exit fullscreen mode
  1. Dependency injection: tip - use an IoC library
Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Hey there. Thanks for these additional examples!

I do agree that my examples are not perfect. I have selected them for simplicity while trying to explain the subject. For anyone who will be reading it and looking for something more optimized and advanced will see your comment and will be able to use it :)

Collapse
 
arnorhs profile image
Arnór Sigurðsson

I agree on most points. The factory one is more like a preference thing - I usually use the object map approach you suggested, rather than a switch statement, but I'd still consider that a preference.

Re: observer.. sure.. but since the article's title is like it's aimed at node.js, it's insane to not talk about EventEmitter - it's the de-facto pub-sub thing to use in node.js and it is used so much in almost every single project - most people won't implement their own observer thing - they'll just use EventEmitter - so that is the most valualble feedback to give on that front.

And regarding decorators.. dude, the state of decorators is in shambles.. I would strongly discourage people from using decorators as much as possible until they become part of standard ecmascript -- they're at stage 3 right now, and with all the differing interfacing between typescript and node.js transpilation etc.. it's a mess better left out if possible.

I do realize a lot of frameworks use them by using the legacy decorator transpilation stuff, but to me that is just an ugly mess, and I'd rather just write plain code until they are fully standardized.

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Hey there,

Thanks for this comment and additional details! Viewers can now see it and get more examples and start using it in their projects :)

Collapse
 
adiozdaniel profile image
adiozdaniel

I've learned a lot

Collapse
 
kuzjt93 profile image
Long.H • Edited

export const redis = new Redis(...);
export default redis;

With this, I think we need to fix the constructor to make sure new Instance are not created?

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

Collapse
 
doronguttman profile image
Doron Guttman

You don't, as you are not exporting the classz just the instance, which is the whole point.

Collapse
 
wormss profile image
WORMSS

You may want to test some of your examples.

You have an error in the first one with capitalising Return this will cause an error.
Also, you have a space between Redis. and instance which is not actually an error, just reads weirdly.

For the Factory pattern, you re-used the variable name for both type of character to create and the name of the created character.
This will obviously work, but just muddies the water for any new developer, and there is not a single advantage to doing so.

You use both direct variable access, character.endurance and getter method action.character.getEndurance() but with no explanation of why one and not the other in either scenario. It leaves getEndurance() as completely unnecessary and muddies the water for no advantage.

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Hey, thank you so much for pointing that out. I fixed first two issues but not sure about the third one. Could you please elaborate which part does not make sense to you? I am not sure what to rewrite :(

Collapse
 
marcus-sa profile image
Marcus S. Abildskov • Edited

What has this got to do with Node or JavaScript? Those are common patterns in any language capable of OOP.
Also the decorator pattern can be very confusing when decorators are typical known for something else in JavaScript...

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

In the article, I mentioned few times that some of these patterns could be observed in frameworks such as Nest.js - I am using this framework on the daily basis and while it is built using TypeScript it is technically Node.js application with Express framework.

I wanted to share some of these patterns that could be used for regular Node.js as well. You can also build Node.js applications with TypeScript -> nodejs.org/en/learn/typescript/int...

I do agree that the decorator pattern could be missleading. Would you recommend to adding a note there that it only applies to Node.js apps built with TypeScript? :)

Collapse
 
marcus-sa profile image
Marcus S. Abildskov

You should have a look at deepkit.io that utilizes TypeScript to the fullest.

Collapse
 
kuzjt93 profile image
Long.H

Nest framework built on top of OOP, I believe

Collapse
 
dsabalete profile image
David Sabalete

Good description of these common 5 patterns. Thank you!

P.S.: In the singleton example, the first "redis" instance is declared as
const medicine = Redis.getInstance();
I'm sure you mean
const redisOne = Redis.getInstance();
don't you?

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Damn, good catch!

Gramarly must have corrected this redisOne into medicine.

Thank you for catching that! :)

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

This post is a cool learning experiment 💪🏼 though I feel like it's necessary to explain that there's no real need for most design patterns mentioned here, specially in JavaScript, a multi-paradigm language.

This might help giving context to beginners:

Cheers!

Collapse
 
aloisseckar profile image
Alois Sečkár

Article has 5 examples, the perex says 10... 🤨

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Good catch! I initially wanted 10 but decided to decrease it to make it shorter. I will fix it right away! :)

Collapse
 
neatshell profile image
Claudio Stella

Five design pattern to know in every OOP capable language, should be a less click bait version of the title.

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Correct, but honestly I was not aiming for click baiting. Node.js is my area of expertise and these design patterns I managed to use by building applications with Nest.js and Nuxt (Vue) where TypeScript is use :)

Collapse
 
manuelsanchez2 profile image
Manuel Sanchez

Your title is far more click bait than the node one