NestJS Decorators

Leveraging the Power of Decorators in NestJS for Enhanced Functionality and Readability

NestJS Decorators

Decorators in NestJS play a crucial role in enhancing the functionality and readability of the code. They are a powerful feature that allows you to modify the behaviour of classes, methods, and properties in a declarative way.

Origin of Decorators

Decorators are a design pattern that originated in the world of object-oriented programming (OOP). They allow behavior to be added to individual objects, without modifying the class code. In JavaScript, decorators were proposed and moved to stage 2 as of 2021. NestJS leverages TypeScript, which supports decorators as an experimental feature, to provide this powerful functionality.

How Decorators Work in NestJS

In NestJS, decorators are used extensively to define and configure its components e.g. routes, inject dependencies, apply guards, interceptors, and much more. They are functions that add metadata to the class and its members, which NestJS uses to create the desired behaviour.

Built-in Decorators in NestJS

NestJS comes with a set of built-in decorators that cover a wide range of functionalities:

Example: Using Built-in Decorators

import { Controller, Get, Param } from '@nestjs/common';

@Controller('users') // Define a controller with the route 'users'
export class UsersController {
  @Get(':id') // Define a GET route that takes an 'id' parameter
  findOne(@Param('id') id: string) { // Extract the 'id' parameter from the request
    return `This action returns user #${id}`; // Return a response with the user ID
  }
}

Creating Custom Decorators

Custom decorators in NestJS are simple to create and can be very powerful. They allow you to encapsulate repetitive logic and reuse it across your application.

Creating a Custom Parameter Decorator

Custom parameter decorators can be used to extract specific data from the request.

Example: Current User Decorator

Let's create a custom decorator to extract the current user from the request.

  1. Define the Decorator:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

// Create a custom parameter decorator named 'CurrentUser'
export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest(); // Get the request object
    return request.user; // Return the user object from the request
  },
);
  1. Use the Decorator:
import { Controller, Get } from '@nestjs/common';
import { CurrentUser } from './current-user.decorator';

@Controller('profile') // Define a controller with the route 'profile'
export class ProfileController {
  @Get() // Define a GET route
  getProfile(@CurrentUser() user: any) { // Use the custom 'CurrentUser' decorator to get the user object
    return user; // Return the user object
  }
}

Creating a Custom Class Decorator

Custom class decorators can be used to add metadata to classes.

Example: Roles Decorator

Let's create a custom decorator to specify roles for a controller.

  1. Define the Decorator:
import { SetMetadata } from '@nestjs/common';

// Create a custom class decorator named 'Roles' that sets metadata 'roles'
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
  1. Use the Decorator:
import { Controller, Get } from '@nestjs/common';
import { Roles } from './roles.decorator';

@Controller('admin') // Define a controller with the route 'admin'
@Roles('admin') // Use the custom 'Roles' decorator to set the role to 'admin'
export class AdminController {
  @Get() // Define a GET route
  getAdminData() {
    return 'Admin data'; // Return admin data
  }
}
  1. Create a Guard to Use the Metadata:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {} // Inject the Reflector service

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler()); // Get the roles metadata
    if (!roles) {
      return true; // If no roles are defined, allow access
    }
    const request = context.switchToHttp().getRequest(); // Get the request object
    const user = request.user; // Get the user object from the request
    return roles.includes(user.role); // Check if the user's role is included in the defined roles
  }
}
  1. Apply the Guard:
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './roles.guard';
import { AdminController } from './admin.controller';

@Module({
  controllers: [AdminController],
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard, // Apply the RolesGuard globally
    },
  ],
})
export class AdminModule {}

Creating a Custom Method Decorator

Custom method decorators can modify the behaviour of methods.

Example: Log Execution Time Decorator

Let's create a custom decorator to log the execution time of a method.

  1. Define the Decorator:
import { performance } from 'perf_hooks';

// Create a custom method decorator named 'LogExecutionTime'
export function LogExecutionTime(): MethodDecorator {
  return (
    target: Object,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<any>,
  ) => {
    const originalMethod = descriptor.value; // Store the original method

    descriptor.value = async function (...args: any[]) {
      const start = performance.now(); // Start timing
      const result = await originalMethod.apply(this, args); // Execute the original method
      const end = performance.now(); // End timing
      console.log(
        `${String(propertyKey)} executed in ${end - start} milliseconds`, // Log the execution time
      );
      return result; // Return the result of the original method
    };

    return descriptor; // Return the modified descriptor
  };
}
  1. Use the Decorator:
import { Controller, Get } from '@nestjs/common';
import { LogExecutionTime } from './log-execution-time.decorator';

@Controller('analytics') // Define a controller with the route 'analytics'
export class AnalyticsController {
  @Get() // Define a GET route
  @LogExecutionTime() // Use the custom 'LogExecutionTime' decorator
  getAnalytics() {
    // Simulate a long-running process
    return new Promise((resolve) => setTimeout(() => resolve('Analytics data'), 2000)); // Return a promise that resolves after 2 seconds
  }
}

Decorators in NestJS are a powerful feature that enhances the readability and functionality of your code. Understanding built-in decorators and creating custom ones can help you build more modular, reusable, and maintainable applications. Whether extracting parameters from requests, adding metadata to classes, or modifying method behaviour, decorators provide a flexible and declarative way to extend your application's capabilities.

Did you find this article valuable?

Support Nicanor Talks Web by becoming a sponsor. Any amount is appreciated!