How to Implement Custom Decorators in NestJS for Cleaner Code

Custom decorators in NestJS enhance code functionality without cluttering main logic. They modify classes, methods, or properties, enabling reusable features like logging, caching, and timing. Decorators improve code maintainability and readability when used judiciously.

How to Implement Custom Decorators in NestJS for Cleaner Code

Decorators in NestJS are a game-changer when it comes to writing clean and maintainable code. They’re like little magic wands that sprinkle some extra functionality onto your classes and methods without cluttering up your main logic. If you’ve been working with NestJS for a while, you’ve probably used built-in decorators like @Controller or @Get. But did you know you can create your own custom decorators? It’s like having a secret superpower!

Let’s dive into the world of custom decorators and see how they can make your NestJS code shine. First things first, what exactly is a decorator? Think of it as a special function that can modify or enhance the behavior of a class, method, or property. It’s like adding a fancy accessory to your code – it doesn’t change the core functionality but adds some extra pizzazz.

In NestJS, creating a custom decorator is surprisingly straightforward. You start by defining a function that returns another function. Sounds a bit inception-like, right? But trust me, it’s not as complicated as it sounds. Here’s a simple example to get us started:

function MyCustomDecorator() {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    console.log('MyCustomDecorator was called!');
    // You can add more logic here
  };
}

Now, you can use this decorator in your NestJS application like this:

class MyClass {
  @MyCustomDecorator()
  myMethod() {
    // Method logic here
  }
}

Every time myMethod is called, you’ll see “MyCustomDecorator was called!” in your console. Pretty neat, huh?

But let’s take it up a notch. Custom decorators really shine when you use them to add reusable functionality across your application. For instance, let’s say you want to log the execution time of certain methods. Instead of adding timing logic to each method, you could create a @Timed decorator:

function Timed() {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const start = Date.now();
      const result = originalMethod.apply(this, args);
      const end = Date.now();
      console.log(`${key} took ${end - start}ms to execute`);
      return result;
    };
    return descriptor;
  };
}

Now you can easily time any method in your application:

class UserService {
  @Timed()
  async findAllUsers() {
    // Method implementation
  }
}

This is just scratching the surface of what’s possible with custom decorators. You could create decorators for logging, error handling, validation, or even role-based access control. The sky’s the limit!

One of my favorite uses for custom decorators is creating a caching mechanism. Imagine you have some expensive operations that you don’t want to repeat unnecessarily. You could create a @Cached decorator that checks if the result is already in cache before executing the method:

import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';

function Cached(ttl: number = 60) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      const cacheKey = `${key}_${JSON.stringify(args)}`;
      const cacheManager = this.cacheManager;
      const cachedResult = await cacheManager.get(cacheKey);
      if (cachedResult) {
        return cachedResult;
      }
      const result = await originalMethod.apply(this, args);
      await cacheManager.set(cacheKey, result, { ttl: ttl * 1000 });
      return result;
    };
    return descriptor;
  };
}

@Injectable()
class ExpensiveService {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  @Cached(300) // Cache for 5 minutes
  async expensiveOperation(param: string) {
    // Simulating an expensive operation
    await new Promise(resolve => setTimeout(resolve, 5000));
    return `Result for ${param}`;
  }
}

This @Cached decorator will automatically cache the results of expensiveOperation for 5 minutes, potentially saving a lot of processing time for repeated calls.

But wait, there’s more! Custom decorators in NestJS aren’t limited to methods. You can also create decorators for classes and properties. Class decorators are particularly useful for adding metadata or altering the class definition. Here’s an example of a class decorator that adds a version property to your class:

function Version(version: string) {
  return function (constructor: Function) {
    constructor.prototype.version = version;
  };
}

@Version('1.0.0')
class MyApi {
  // Class implementation
}

const api = new MyApi();
console.log(api['version']); // Outputs: 1.0.0

Property decorators, on the other hand, can be used to modify how a property behaves. For instance, you could create a @Uppercase decorator that automatically converts a string property to uppercase:

function Uppercase() {
  return function (target: any, key: string) {
    let value = target[key];

    const getter = function() {
      return value;
    };

    const setter = function(newVal: string) {
      value = newVal.toUpperCase();
    };

    Object.defineProperty(target, key, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  };
}

class User {
  @Uppercase()
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const user = new User('john');
console.log(user.name); // Outputs: JOHN

Now, you might be wondering, “This all sounds great, but how do I test these custom decorators?” Great question! Testing decorators can be a bit tricky, but it’s definitely doable. Here’s a simple example using Jest:

describe('Timed Decorator', () => {
  it('should log execution time', () => {
    const consoleSpy = jest.spyOn(console, 'log');
    
    class TestClass {
      @Timed()
      testMethod() {
        // Simulate some work
        for (let i = 0; i < 1000000; i++) {}
      }
    }

    const instance = new TestClass();
    instance.testMethod();

    expect(consoleSpy).toHaveBeenCalledWith(expect.stringMatching(/testMethod took \d+ms to execute/));
    
    consoleSpy.mockRestore();
  });
});

This test creates a TestClass with a method decorated by @Timed, calls the method, and then checks if the console.log was called with a message matching the expected format.

As you dive deeper into custom decorators, you’ll discover that they’re an incredibly powerful tool for keeping your codebase DRY (Don’t Repeat Yourself) and for separating concerns. They allow you to extract cross-cutting concerns like logging, error handling, or performance monitoring into reusable pieces of code.

But remember, with great power comes great responsibility. While decorators can make your code cleaner and more maintainable, overusing them or using them for complex logic can sometimes make your code harder to understand. As with any programming technique, use them judiciously and always consider readability and maintainability.

In my experience, custom decorators really shine when you’re working on larger NestJS projects. They become a sort of secret weapon, allowing you to elegantly solve problems that would otherwise require repetitive or messy code. I remember working on a project where we needed to implement rate limiting for our API endpoints. Instead of cluttering our controller methods with rate-limiting logic, we created a @RateLimit decorator. It was a game-changer – suddenly, adding rate limiting to any endpoint was as simple as slapping on a decorator.

As you continue your NestJS journey, I encourage you to experiment with custom decorators. Start small – maybe create a simple logging decorator or a decorator for input validation. As you get more comfortable, you can tackle more complex use cases. Before you know it, you’ll be wielding the power of custom decorators like a pro, creating cleaner, more maintainable NestJS applications.

Remember, the goal is to make your code more readable and maintainable, not to show off how clever you can be with decorators. Use them where they make sense, and your future self (and your teammates) will thank you. Happy coding!