How to Create Custom Decorators in NestJS

Updated: January 1, 2024 By: Guest Contributor Post a comment

Introduction

Custom decorators in NestJS provide a powerful way to enhance the functionality of your classes, methods, and parameters while keeping your codebase clean and maintainable. Learn how to implement them to leverage metadata and AOP techniques effectively.

Understanding Decorators

Before diving into custom decorators, it’s important to understand what decorators are and how they’re used in TypeScript and NestJS. A decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

Decorators are a Stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript. In NestJS, they are used extensively, for example in declaring modules, controllers, and dependencies.

Basic Custom Decorator

To create a basic custom decorator in NestJS, you first need to define a function that will serve as the decorator itself. The function can receive different arguments, depending on what the decorator is applied to.

function Log(message: string) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      console.log(`{message}: {target.constructor.name}.{propertyKey}`)
    };
  }

  class MyClass {
    @Log('Executing')
    myMethod() {}
  }

In this example, Log is a method decorator where message is the custom metadata you can pass to it. This decorator simply logs a message combined with the class and method names whenever a method decorated with @Log gets called.

Parameter Decorators

Parameter decorators can customize and provide extra information about a method’s parameters. Here’s how you can define a custom NestJS parameter decorator:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  return data ? request.user[data] : request.user;
});

@Controller('account')
export class AccountController {
  @Get('profile')
  getProfile(@User() user) {
    return user;
  }
  
  @Get('email')
  getEmail(@User('email') email: string) {
    return email;
  }
}

Here, User is a custom parameter decorator used to fetch user information from the request object, giving you quick access to any user-related data within an endpoint.

Advance Usage of Custom Decorators

For more advanced scenarios, you can define a decorator that enhances the behavior of the method it decorates, for example, implementing caching.

import { Cache } from './cache.decorator';

const ttl = 60; // Time to live: 60 seconds

class Service {
  @Cache(ttl)
  expensiveOperation() {
    // perform the operation
  }
}

The Cache decorator in this case would be responsible for checking if the result of the costly operation has already been cached and return it if so, reducing execution time.

Custom Class Decorators

Class decorators can act upon the constructor of a class and modify its behavior or replace the class entirely. Below is an example of a logging class decorator applied to a service.

function ServiceLogger() {
  return function (constructor: Function) {
    console.log(`Creating service instance: ${constructor.name}`);
  };
}

@ServiceLogger()
export class MyService {
  // class definition
}

This decorator logs out a message whenever an instance of MyService is created, which could be helpful for debugging purposes.

Integrating with NestJS Dependency Injection

In NestJS, custom decorators often play nicely with the framework’s dependency injection system. You can create decorators that augment the DI mechanism, like this custom decorator that binds custom metadata to route method parameters.

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

@Controller('users')
export class UsersController {
  @Get('')
  @Roles('admin')
  async findAll() {
    // ... Only allow admin users
  }
}

Roles uses SetMetadata utility function to assign ‘roles’ metadata to a controller’s method. Nest’s DI system can then read this metadata and enforce role-based access accordingly.

Testing Custom Decorators

Like any part of your application, custom decorators should be thoroughly tested. When creating tests for your decorators, you’re generally looking at how they modify the class, method, or parameter behavior and ensuring that they work as expected. NestJS provides a powerful testing module that you can use to mock dependencies and evaluate decorators in an isolated environment.

// Example test could look like this, using Jest
describe('@Log decorator', () => {
  it('should log the correct message', () => {
    const logSpy = jest.spyOn(console, 'log');
    const myClass = new MyClass();
    myClass.myMethod();
    expect(logSpy).toHaveBeenCalledWith('Executing: MyClass.myMethod');
    logSpy.mockRestore();
  });
});

Conclusion

In summary, custom decorators in NestJS offer an elegant way to add behavior and encapsulate logic seamlessly within your application’s architecture. Starting from simple method enhancements to more sophisticated parameter decorations and class transformations, NestJS decorators empower developers to write dry, organized, and declarative code. The provided examples just scratch the surface of the possibilities, inviting you to explore and create your customized solutions.