Node.js, the popular JavaScript runtime, helps developers build complex backend systems. With so many capabilities, it can get quite challenging to work with, and hence, design patterns are used.Design patterns help developers write high-quality, testable, and maintainable code.
Some of the design patterns are built into Node.js, and some can be applied to other programming languages.In this article, we will discuss what the design patterns in Node.js are and the top Node.js design patterns that developers can use in 2024.
What are Design Patterns in Node.js?Design patterns in Node.js are some of the most optimal solutions to common Node.js development problems. They enable developers to write better, more scalable, and more maintainable Node.js code. Developers often use one of the design patterns whenever they are stuck on an issue.
There are plenty of design patterns available for Node.js. However, it is important to note that they should be used judiciously.
Design patterns should not be forcefully applied where they are not required.
Top Node.js Design Patterns in 2024
There are various design patterns in Node.js, but some of the best ones are mentioned below:-
IIFE are functions that are invoked as soon as they are declared. This common design pattern can be used in Node.js for a couple of things:
- Encapsulation: Code is encapsulated within local scope.
- Privacy: Variables and functions cannot be used outside the scope.
Consider the code below:
Node
(function (parameter) {
const a = parameter;
const b = 20;
const answer = a * b;
console.log(`The answer is ${answer}`);
})(4);
- We have defined a function that is immediately invoked by passing “4” as the parameter.
- It simply multiplies “a” and “b” and assigns the value to “answer” before logging the string on the console.
The output is:
The answer is 80
2. Module PatternOne of the most fundamental design patterns in Node.js, the module pattern is used to separate and encapsulate some code into different modules. This pattern helps to organize the code and hide the implementation details as well.
For example, look at the code below:
Node
// module.js
const firstName = "Mark";
const displayFirstName = () => {
return `Hi, ${firstName}!`;
};
const lastName = "Harris";
const displayLastName = () => {
return `Hi, ${lastName}!`;
};
module.exports = { lastName, displayLastName };
In module.js:
- We have assigned two different strings to two different variables, “firstName” and “lastName”.
- There are two functions “displayFirstName” and “displayLastName”.
- Lastly, we have exported “lastName” and “displayLastName”.
Node
// index.js
const modules = require("./module");
console.log(modules.displayLastName());
In index.js (above code), we have imported the module.js file, and we are logging out “displayLastName” which outputs:
Hi, Harris!
In the above example, we have used the module pattern that hides the implementation details of the variables and functions defined in module.js. Notice that we can only use “lastName” and “displayLastName” in index.js as we have only exported those two.
This means, we have access to only “lastName” and “displayLastName”. On the other hand, “firstName” and “displayFirstName” are not accessible from index.js.
3. Event-Driven PatternThe event-driven pattern utilizes the event-driven architecture of Node.js to handle events. For handling events, it uses the EventEmitter class. An event emitter enables developers to raise an event from any part of the application that can be listened to by a listener and an action can be performed.
Look at the code shared below:
Node
const EventEmitter = require("events");
const emitter = new EventEmitter();
emitter.on("someEvent", () => {
console.log("An event just took place!");
});
emitter.emit("someEvent");
- Here, we have imported the events module.
- We have created an instance of the EventEmitter class.
- We have registered an event titled “someEvent”, which takes a callback function.
- Finally, we have triggered the event using the “emit” method. When the event is emitted or triggered, the callback function associated with “someEvent” gets called.
Output on the console:
An event just took place!
The events executed by event emitters are executed synchronously. This pattern helps developers use event-based programming in Node.js.
4. Singleton PatternThe singleton pattern is one of the most popular design patterns in programming languages. In Node.js, the singleton pattern is used where there is a requirement for only one instance of a class.
Let’s understand this with the code.
Node
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
displayString() {
console.log("This is a string.");
}
}
const firstInstance = new Singleton();
const secondInstance = new Singleton();
console.log(firstInstance === secondInstance); // true
firstInstance.displayString();
In the above code:
- We have created a class titled “Singleton”.
- Firstly, we have checked if an instance of the class exists, if it does not exist, we are assigning the current instance of the class to “Singleton.instance”. If the instance already exists, we simply return the existing instance. This ensures that all the instances of this class reference the same object.
- Next, we have defined a method that displays a string on the console.
- We have created two instances of the “Singleton” class: “firstInstance” and “secondInstance”.
- We have checked if both instances reference the same object.
- Lastly, we have called the “displayString” method on “firstInstance”.
Output:
true
This is a string.
5. Factory PatternA type of creational design pattern, the factory pattern uses a single object that works as a factory to create new objects. This design pattern hides the implementation logic and enhances flexibility and loose coupling in terms of object creation.
In the code below, we have created a “Motorcycle” class containing the name and the brand.
Node
class Motorcycle {
constructor(name, brand) {
this.name = name;
this.brand = brand;
}
}
class MotorcycleFactory {
createMotorcycle(type) {
switch (type) {
case "hunter":
return new Motorcycle("Hunter 350", "Royal Enfield");
case "ronin":
return new Motorcycle("Ronin", "TVS Ronin");
default:
throw new Error("Invalid motorcycle type");
}
}
}
const factory = new MotorcycleFactory();
const productA = factory.createMotorcycle("hunter");
const productB = factory.createMotorcycle("ronin");
console.log(productA.name); // Hunter 350
console.log(productB.name); // Ronin
- Next, we have created a “MotorcycleFactory” class containing a method that creates a new instance of the “Motorcycle” class based on the type of motorcycle.
- We have created an instance of the “MotorcycleFactory” class titled “factory”. We use this instance to create other instances of the class.
- Lastly, we define the two products having different motorcycle types passed on the “createMotorcycle” method.
6. Dependency Injection PatternThe dependency injection pattern enables classes to be more modular and testable, along with being loosely coupled. It does so by injecting dependencies into other classes instead of creating the dependencies inside the classes.
The code given below has two classes:
- Display: It has a method called “displayMessage” that simply logs a message.
- Employee: It has two variables and a method called “show”, which takes in the method of class “Display” and passes the message as a parameter.
Node
class Display {
displayMessage(message) {
console.log(message);
}
}
class Employee {
constructor(display, name) {
this.display = display;
this.name = name;
}
show() {
this.display.displayMessage("Employee's name is " + this.name);
}
}
const display = new Display();
const user = new Employee(display, "John");
user.show(); // Employee's name is John
- Notice that “display” is injected as a dependency by passing it as a parameter and assigning it to “this.display”.
- The injected “display” is used inside the “show” method, and the “displayMessage” is called.
- In the later stages, a new instance of class “Display” is created.
- Along with it, the instance of class Employee is also created, and it passes two parameters inside it. We have passed the “display” instance as a parameter as well, and this is the dependency injection.
- Finally, the method “show” is called on the “user” instance.
Output:
Employee’s name is John
7. Middleware PatternExpress, a popular Node.js framework has the concept of middlewares, which are very useful in performing certain tasks. Middleware functions are those functions that perform tasks between the request and response of an API call. Middlewares have access to the request and response objects.
This is how we define middleware in Node.js/Express:
Node
const express = require("express");
const app = express();
app.use((req, res, next) => {
console.log("This is a Middleware");
next();
});
- We use the “use()” method to define a middleware, and it has three arguments: req, res, and next.
- The “next()” function is used to pass the control to the next middleware or a route handler.
Here is a working example of a middleware pattern.
Node
const express = require("express");
const app = express();
app.use((req, res, next) => {
console.log("This is a Middleware");
next();
});
app.get("/", (req, res) => {
res.send("GET request handled!");
});
app.listen(4000, () => {
console.log("listening on port 4000");
});
In the above code, when we hit “http://localhost:4000”, on the browser, we see “GET request handled!” displayed on the page. But on the console, we see:
This is a Middleware
This is because:
- When we hit the route “/”, the control of the Node.js code first passes through the middleware.
- When it encounters next(), it goes to the next middleware or a route handler, in this case “/”.
- Finally, the GET request is hit.
8. Promise PatternA lot of times, developers need to handle asynchronous operations in Node.js, and that’s where a promise comes in. The promise pattern is extremely powerful in the sense that it helps to execute asynchronous operations in a sequential manner.
Firstly, a promise can be created by instantiating a Promise class, which takes a couple of parameters: “resolve” and “reject”. Look at the code given below to understand.
Node
const myPromise = new Promise((resolve, reject) => {
let x = 5;
// Asynchronous operation
setTimeout(() => {
if (x > 0) {
// If operation is successful
reject("Success");
} else {
// If operation fails
reject("Failure");
}
}, 2000);
});
myPromise
.then((res) => console.log(res))
.catch((err) => console.error(err));
- Inside the promise, there is a “setTimeout” function that runs after 2 seconds. If the variable “x” is greater than 0, it resolves the promise; otherwise, it rejects it.
- We can make use of “then()” and “catch()” methods to consume the promise. If the promise is resolved, the “then()” method handles it.
- If the promise is rejected, the “catch()” method handles the error.
In the above code, after 2 seconds, the promise gets resolved, and if it is resolved, the control goes to the “then()” method, and “Success” is printed on the console.
Output:
Success
ConclusionNode.js design patterns enables developers to write highly modular and testable code. In this article, we discussed what the design patterns are in Node.js and some of the best Node.js design patterns. Understanding and using design patterns on the actual code will not only solve a problem but they will enhance the performance as well.
Frequently Asked QuestionsWhy should design patterns be used in Node.js?Node.js developers deal with concepts such as servers, modules, events, file system etc., and these concepts often come with a plethora of problems. Design patterns are the solutions to these problems. They help developers improve code reusability and make the code easily testable.
How many design patterns are there in Node.js?Node.js design patterns can be majorly classified by 4 types: Singleton, Factory, Command, Observer. But there are many more design patterns in Node.js, and books have been written on some of them. All the design patterns are helpful in different scenarios. Some of them are built-in and others can be applied to OOPS as well as non-OOPS paradigms.
3. In Node.js, are there any new design patterns?Yes, new design patterns keep being created and introduced among Node.js developers. This is because Node.js is used by millions of JavaScript developers, and with such a large community, there are chances of running into different problems. Also, the backend part is becoming more complex with the latest topics like serverless and microservices, so to tackle these problems, new design patterns are introduced.
|