Design patterns are reusable solutions to common software design problems. These patterns can help you create software that is both scalable and maintainable. NestJS is a popular framework for building server-side applications using Node.js. It is designed to provide a solid architecture for your applications, with built-in support for features like dependency injection and modules. In this article, we will discuss some of the most common design patterns used in NestJS and how you can implement them in your own applications.
Introduction
Design patterns have been used in software development for decades. They are a way to solve common problems that developers face when designing software. NestJs is a popular framework for building server-side applications using Node.js. It provides a solid architecture for your applications, with built-in support for features like dependency injection and modules. By using design patterns in NestJs, you can create software that is both scalable and maintainable.
Creational Patterns
Singleton
The singleton pattern is a creational pattern that ensures that a class has only one instance and provides a global point of access to that instance. In NestJs, you can use the @Injectable() decorator to make a class a singleton. This ensures that there is only one instance of the class and that it can be easily accessed from anywhere in your application.
@Injectable({ scope: Scope.DEFAULT })
export class LoggerService {
log(message: string) {
console.log(message);
}
}
Factory Method
The factory method pattern is a creational pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. In NestJs, you can use a factory provider to create objects of different types based on the context in which they are created.
export interface IProduct {
name: string;
price: number;
}
@Injectable()
export class ProductFactory {
createProduct(type: string): IProduct {
if (type === 'book') {
return new Book();
} else if (type === 'movie') {
return new Movie();
} else if (type === 'music') {
return new Music();
} else {
throw new Error(`Invalid product type: ${type}`);
}
}
}
export class Book implements IProduct {
name = 'Book';
price = 10;
}
export class Movie implements IProduct {
name = 'Movie';
price = 20;
}
export class Music implements IProduct {
name = 'Music';
price = 5;
}
Builder
The builder pattern is a creational pattern that separates the construction of an object from its representation. It allows you to create complex objects step by step, with each step taking care of a specific aspect of the object's construction. In NestJS, you can use the builder pattern to create complex objects like database connections or API clients.
@Injectable()
export class DatabaseConnectionBuilder {
private host: string;
private port: number;
private username: string;
private password: string;
private database: string;
setHost(host: string) {
this.host = host;
return this;
}
setPort(port: number) {
this.port = port;
return this;
}
setUsername(username: string) {
this.username = username;
return this;
}
setPassword(password: string) {
this.password = password;
return this;
}
setDatabase(database: string) {
this.database = database;
return this;
}
build() {
return new DatabaseConnection(
this.host,
this.port,
this.username,
this.password,
this.database,
);
}
}
export class DatabaseConnection {
constructor(
private readonly host: string,
private readonly port: number,
private readonly username: string,
private readonly password: string,
private readonly database: string,
) {
// connect to database
}
// other methods
}
Structural Patterns
Decorator
The decorator pattern is a structural pattern that allows you to add behavior to an object dynamically, without affecting its class. It is useful when you want to add functionality to an object at runtime. In NestJs, you can use the @UseInterceptors() decorator to add behavior to a class or method.
@UseInterceptors(LoggingInterceptor)
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
}
Facade
The facade pattern is a structural pattern that provides a simple interface to a complex subsystem. It is useful when you want to simplify a complex system by providing a high-level interface that clients can use to interact with the system. In NestJs, you can use a service as a facade for a complex subsystem.
@Injectable()
export class PaymentService {
constructor(
private readonly creditCardService: CreditCardService,
private readonly bankService: BankService,
) {}
processPayment(payment: Payment) {
const isCreditCard = this.creditCardService.validate(payment.cardNumber);
if (isCreditCard) {
return this.creditCardService.processPayment(payment);
} else {
return this.bankService.processPayment(payment);
}
}
}
Adapter
The adapter pattern is a structural pattern that allows incompatible interfaces to work together. It is useful when you want to reuse existing code that has a different interface than what you need. In NestJs, you can use an adapter to convert the interface of one module to the interface of another module.
@Injectable()
export class UserServiceAdapter {
constructor(private readonly userRepository: UserRepository) {}
async getUsers(): Promise<User[]> {
const users = await this.userRepository.find();
return users.map((user) => ({
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
}));
}
}
Proxy
The Proxy Design Pattern is a structural pattern that provides a surrogate or placeholder for another object to control access to it. In NestJS, we can use the Proxy Design Pattern to control access to a resource or service.
import { Injectable } from '@nestjs/common';
import { MyService } from './my.service';
interface MyServiceInterface {
doSomething(): void;
}
@Injectable()
class MyServiceProxy implements MyServiceInterface {
private myService: MyService;
constructor() {
this.myService = new MyService();
}
doSomething() {
console.log('Proxy: Before method call');
this.myService.doSomething();
console.log('Proxy: After method call');
}
}
The MyService class provides a method to do something. The MyServiceProxy class acts as a proxy for the MyService class, intercepts the method call, and adds additional functionality before and after the method call.
Behavioral Patterns
Observer
The observer pattern is a behavioral pattern that allows one object to notify other objects when its state changes. It is useful when you want to decouple the observer from the subject, so that the observer can be easily replaced or removed. In NestJs, you can use the EventEmitter class to implement the observer pattern.
@Injectable()
export class UserService {
private readonly users = new Map<number, User>();
private readonly userCreated = new EventEmitter<User>();
createUser(user: CreateUserDto) {
const id = Math.max(...this.users.keys()) + 1;
const newUser = { id, ...user };
this.users.set(id, newUser);
this.userCreated.emit(newUser);
return newUser;
}
onUserCreated(): Observable<User> {
return this.userCreated.asObservable();
}
}
Strategy
The strategy pattern is a behavioral pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. It is useful when you want to select an algorithm at runtime. In NestJs, you can use a strategy to implement different authentication methods.
export interface AuthenticationStrategy {
authenticate(request: Request): Promise<User>;
}
@Injectable()
export class JwtAuthenticationStrategy implements AuthenticationStrategy {
async authenticate(request: Request): Promise<User> {
const token = request.headers['authorization'];
const user = await this.authService.verifyToken(token);
if (!user) {
throw new UnauthorizedException('Invalid token');
}
return user;
}
}
@Injectable()
export class BasicAuthenticationStrategy implements AuthenticationStrategy {
async authenticate(request: Request): Promise<User> {
const authorizationHeader = request.headers['authorization'];
if (!authorizationHeader) {
throw new UnauthorizedException('Authorization header missing');
}
const [username, password] = Buffer.from(
authorizationHeader.split(' ')[1],
'base64',
)
.toString()
.split(':');
const user = await this.authService.authenticateUser(username, password);
if (!user) {
throw new UnauthorizedException('Invalid username or password');
}
return user;
}
}
@Injectable()
export class AuthService {
constructor(private readonly userService: UserService) {}
async verifyToken(token: string): Promise<User> {
// verify token and return user
}
async authenticateUser(username: string, password: string): Promise<User> {
// authenticate user and return user
}
}
Chain of Responsibility
The chain of responsibility pattern is a behavioral pattern that allows you to chain multiple handlers, where each handler can either handle the request or pass it on to the next handler in the chain. It is useful when you want to decouple the sender of a request from its receiver. In NestJs, you can use middleware to implement the chain of responsibility pattern.
@Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
constructor(private readonly authService: AuthService) {}
async use(request: Request, response: Response, next: NextFunction) {
try {
const user = await this.authService.authenticate(request);
request.user = user;
next();
} catch (error) {
next(error);
}
}
}
@Injectable()
export class AuthorizationMiddleware implements NestMiddleware {
async use(request: Request, response: Response, next: NextFunction) {
try {
const user = request.user;
if (!user.isAdmin) {
throw new ForbiddenException('You are not authorized');
}
next();
} catch (error) {
next(error);
}
}
}
Template Method
The Template Method Design Pattern is a behavioral pattern that defines the skeleton of an algorithm in a base class and allows subclasses to override some steps of the algorithm without changing its structure. In NestJS, we can use the Template Method Design Pattern to define a base class that provides a common algorithm for handling requests, and allow subclasses to customize parts of the algorithm.
import { Injectable } from '@nestjs/common';
abstract class MyBaseController {
async handleRequest() {
this.validateRequest();
const result = await this.processRequest();
this.sendResponse(result);
}
protected abstract validateRequest(): void;
protected abstract async processRequest(): Promise<any>;
protected abstract sendResponse(result: any): void;
}
@Injectable()
class MyController extends MyBaseController {
protected validateRequest() {
console.log('Validating request...');
}
protected async processRequest(): Promise<any> {
console.log('Processing request...');
return { data: 'response' };
}
protected sendResponse(result: any) {
console.log('Sending response...');
console.log(result);
}
}
The MyBaseController class provides a common algorithm for handling requests, and defines three abstract methods that subclasses must implement. The MyController class overrides these methods to customize the validation, processing, and response handling steps.
Conclusion
In conclusion, design patterns are a powerful tool for building robust and maintainable applications. In NestJs, you can use a variety of design patterns, such as creational patterns, structural patterns, and behavioral patterns, to solve common problems and improve the quality of your code. By understanding and applying these patterns, you can write cleaner, more modular, and more reusable code that is easier to test, maintain, and evolve over time.
Top comments (1)
Perfectly explained. Thanks for sharing.