How can you implement Singleton Design Pattern in NestJs?

KAZI AKIB JAVED
5 min readApr 18, 2024

--

What is design pattern?

Design patterns are like pre-built tools for software development. They tackle problems that programmers encounter frequently, such as making code flexible, adaptable, and secure. These tools offer a structured way to solve these issues, leading to cleaner, more manageable code. By using design patterns, developers can craft higher-quality software faster.

Why?

Design patterns are established, well-defined approaches to address common programming challenges. They offer a structured way to solve problems, promoting best practices for software architecture and leading to more:

  • Robust code: Design patterns enhance code reliability by providing tested solutions.
  • Maintainable code: They make code easier to understand, modify, and debug due to their clear structure.
  • Extensible code: Design patterns facilitate the addition of new features without extensive code overhauls.
  • Reusable code: You can often leverage design patterns across different parts of your application or even in other projects.

Types of Design Patterns

Design patterns can be categorized into three main groups based on their focus:

  1. Creational Patterns: These patterns deal with object creation in a flexible and efficient way. Common examples in Node.js include:
  • Module Pattern: Encapsulates code and data within a module, promoting code organization and reusability.
  • Factory Method Pattern: Centralizes object creation, allowing for dynamic selection of object types at runtime.
  • Singleton Pattern: Ensures a class has only one instance, useful for global state management (use cautiously in Node.js due to potential for tight coupling).
  • Prototype Pattern: Creates objects by cloning an existing prototype, simplifying object creation with shared properties and methods.

2. Structural Patterns: These patterns focus on how classes and objects are composed to form larger structures. Some popular ones in Node.js are:

  • Adapter Pattern: Makes incompatible interfaces work together by providing a wrapper that translates calls between them.
  • Facade Pattern: Offers a simplified interface for a complex system, hiding implementation details.
  • Decorator Pattern: Dynamically adds new functionalities to an object without modifying its structure.

3. Behavioral Patterns: These patterns define how objects communicate and collaborate to achieve a specific task. Examples often used in Node.js include:

  • Observer Pattern: Establishes a one-to-many relationship where an object (subject) notifies its dependents (observers) about changes in its state. Useful for event-driven architectures and real-time updates.
  • Callback Pattern: Passes a function as an argument to be executed after an asynchronous operation completes.
  • Promise Pattern: Represents the eventual completion (or failure) of an asynchronous operation. Promises provide cleaner error handling and chaining of asynchronous operations compared to callbacks.
  • Mediator Pattern: Defines a central object (mediator) that coordinates communication between a set of objects, reducing dependencies between them.

By understanding design patterns and applying them judiciously, you can significantly enhance the quality, maintainability, and scalability of your Node.js applications.

Example of Singleton Design Pattern in NestJs

Singleton Service: Create a service that should be instantiated only once throughout the application.

// singleton.service.ts
export class SingletonService {
private instance: SingletonService;

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

Now you have never seen the code in constructor like this. So how nestjs instantiate each service once, that’s the question in your mind. right?

Well, In NestJS, services are by default singletons. When you declare a service using the @Injectable() decorator, NestJS automatically manages it as a singleton within the scope of the module. This means that the same instance of the service is shared across all the components (controllers, other services, etc.) within the same module.

So we use something like this:

// singleton.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class SingletonService {

constructor() {}

getAllUsers = () =>{
//Your code
}
}

However, you can still leverage the Singleton pattern indirectly within your NestJS application to manage shared resources or stateful components. For instance:

  1. Database Connection Pooling: You can use the Singleton pattern to create a single database connection pool that is shared across multiple API endpoints. This ensures efficient resource utilization and prevents unnecessary overhead from creating multiple connections.
  2. Caching Mechanism: Implement a caching mechanism using the Singleton pattern to store frequently accessed data in memory. This can improve the performance of your API by reducing the need to fetch data from external sources repeatedly.
  3. Configuration Management: Use a Singleton pattern to manage application configuration settings. This ensures that configuration values are loaded only once and can be accessed globally throughout your application.
  4. Logging Service: Implement a logging service as a Singleton to centralize logging functionality across your API endpoints. This allows you to maintain consistent logging behavior and easily manage log levels and destinations.
  5. Authentication and Authorization: Utilize a Singleton pattern to manage authentication and authorization logic, such as a shared authentication service that handles user authentication tokens or session management.

Authentication and Authorization example using Singleton Design Pattern:

let’s create a basic example of implementing Authentication and Authorization using the Singleton pattern in a NestJS application.

First, let’s create a AuthService that handles user authentication:

// auth.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AuthService {
private static instance: AuthService;

private constructor() {}

static getInstance(): AuthService {
if (!AuthService.instance) {
AuthService.instance = new AuthService();
}
return AuthService.instance;
}

async validateUser(username: string, password: string): Promise<boolean> {
// Here, you would typically validate the user against your database or some other data source
// For simplicity, we'll just return true if the username and password match
return username === 'admin' && password === 'password';
}
}

Next, let’s create an AuthGuard that will be responsible for protecting routes by checking if the user is authenticated:

// auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
private authService: AuthService;

constructor() {
this.authService = AuthService.getInstance();
}

canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { username, password } = request.body;

return this.authService.validateUser(username, password);
}
}

Finally, let’s create a simple controller with a protected route that requires authentication:

// app.controller.ts
import { Controller, Post, UseGuards, Body } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller()
export class AppController {
@Post('login')
@UseGuards(AuthGuard)
async login(@Body() body) {
return { message: 'Login successful' };
}

@Post('public')
async publicEndpoint(@Body() body) {
return { message: 'This is a public endpoint' };
}
}

In this example:

  • The AuthService is implemented as a Singleton to ensure that there is only one instance throughout the application.
  • The AuthGuard is responsible for checking the user's credentials against the AuthService.
  • The AppController contains two routes: /login, which is protected by the AuthGuard, and /public, which is accessible without authentication.

With this setup, only authenticated users can access the /login route, while the /public route is accessible to everyone. This demonstrates a basic implementation of authentication and authorization using the Singleton pattern in NestJS.

--

--