DEV Community

Vincent Jang
Vincent Jang

Posted on

How to make Useful Decorator in NestJS

Hello devs!
I'm backend Developer Phantola (pandora)!
I wanna share a how to make a decorator in NestJS.

First of all, In my opinion, NestJS is more close NodeJS(also TS) DI framework. As you can see a official document in NestJS, They insist agnosticism about framework.

So if you want to a software use DI, NestJS will be attractive option for you.

If you have a little bit of experience using NestJS, Decorator is very powerful and important feature for NestJS.

Personally, I worked in 3yr in web3 platform, I made and manage a few servers, I just want to share a experience about make a decorator more useful.

Let's do this.

This article provide a contents with few steps. and We make a provider decorator(Class decorator)

  1. Decide a Decorator name and Symbol.
  2. Make a module
  3. Implement OnModuleInit
  4. Apply Decorator in your code

Step 1. Decide a Decorator name and Symbol.

In this example, I will make a error catch decorator.
So, Decorator Name will be @CatchError and internal symbol will be CATCH_ERROR_DECORATOR.

// catch-error.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const CatchErrorDecoratorSymbol = Symbol('CATCH_ERROR_DECORATOR')
export const CatchError = () => SetMetadata(CatchErrorDecoratorSymbol, true);

Enter fullscreen mode Exit fullscreen mode

Then you can use @CatchError Decorator in your logics.

Step 2. Make a module for decorator

We make a module for CatchError decorator.
As you know, typescript decorator is work like function composition in math.

You can see a detail description in here

Decorator is more like wrapper function. So we need to define how to our decorator works.

Then, Let's Make a module

// catch-error.module.ts

@Module({
  imports: [DiscoveryModule],
  providers: [],
  exports: [],
})
export class CatchErrorModule implements OnModuleInit {
  constructor() {}

  onModuleInit() {}
}

Enter fullscreen mode Exit fullscreen mode

After make a module, You can import a module which you want.

Step3. Implement OnModuleInit

After this step, Our decorator will be define when module init lifecycle

Before we implement a lifecycle function, we need to Inject few services.

// catch-error.module.ts

@Module({
  imports: [DiscoveryModule],
  providers: [],
  exports: [],
})
export class CatchErrorModule implements OnModuleInit {
  constructor(
    private readonly discoveryService: DiscoveryService,
    private readonly metadataScanner: MetaDataScanner,
    private readonly reflector: Reflector

  ) {}

  onModuleInit() {}
}

Enter fullscreen mode Exit fullscreen mode

I talk a services briefly.

DiscoveryService, MetaDataScanner, Reflector is very important for useful metadata.

DiscoveryService can find a provider and controller instances.

MetaDataScanner provide a function for get list of class methods.

At Last, Reflector can use globally, It provides a get/set metadata

Then, start again.

// inside a CatchErrorModule

onModuleInit() {

 return this.discoveryService
      .getProviders() // Find all providers
      .filter((wrapper) => wrapper.isDependencyTreeStatic())
      .filter(({ instance }) => instance && Object.getPrototypeOf(instance))
      .forEach(({ instance }) => {

        // Find and filtering decorator applied classes
        const isDecoratorApplied = this.reflector.get(CatchErrorDecoratorSymbol, instance.constructor)

        if(!isDecoratorApplied) return;

        const providerMetaKeys = Reflect.getOwnMetadataKeys(
          instance.constructor,
        );
        const providerMetaDatas = providerMetaKeys.map((k) => [
          k,
          Reflect.getMetadata(k, instance.constructor),
        ]);


        // Get all methods in provider which decorator applied
       this.metadataScanner.getAllMethodNames(instance).forEach((method) => {

          const methodRef = instance[method];
          const methodMetaKeys = Reflect.getOwnMetadataKeys(methodRef);
          const methodMetaDatas = methodMetaKeys.map((k) => [
            k,
            Reflect.getMetadata(k, methodRef),
          ]);


          // wrapping code here


          // Preserve exist metadatas for method
          methodMetaDatas.forEach(([k, v]) =>
            Reflect.defineMetadata(k, v, instance[method]),
          );

      })

      // Preserve exist metadatas for provider
      providerMetaDatas.forEach(([k, v]) =>
        Reflect.defineMetadata(k, v, instance.constructor),
      );
   })
}

Enter fullscreen mode Exit fullscreen mode

So, In this step, We implement a decorator initializing code use onModuleInit lifecycle. then, If your Nest Application start, and module initialized, your decorator will be decorate a methods.

4. Apply Decorator in your code.


// your provider
@Injectable()
@CatchError()
export class ExampleService {
  constructor() {}

  pong() {
    return {
      code: 200,
      message: 'pong',
    };
  }

  async decoratorTest() {
    throw new Error('Error occurred')
  }
}

Enter fullscreen mode Exit fullscreen mode

Thanks for reading.

This decorator evaluation mechanism is very helpful if you want inject another services, or another dependencies.

I hope a your application more dynamically.
also, I hope this helps even a little bit.

Top comments (0)