DEV Community

Viraj Lakshitha Bandara
Viraj Lakshitha Bandara

Posted on

Exploring Custom Decorators in NestJS: Enhancing Your API with Metadata and Reusability

topic_content

Exploring Custom Decorators in NestJS: Enhancing Your API with Metadata and Reusability

NestJS, a progressive Node.js framework, has gained significant popularity for building scalable and maintainable server-side applications. Inspired by Angular, it leverages TypeScript's decorators to promote code reusability and maintainability. Decorators in NestJS allow developers to inject metadata and modify the behavior of classes, methods, and properties. This blog post delves into the world of custom decorators in NestJS, exploring their creation, implementation, and the vast potential they unlock in building robust APIs.

Understanding Decorators in NestJS

Decorators, at their core, are functions that modify the behavior of the decorated declaration (class, method, or property) without directly altering its source code. They provide an elegant way to separate cross-cutting concerns like logging, validation, and authorization from your business logic. NestJS heavily relies on decorators provided by the framework itself for defining controllers, routes, providers, and more.

Let's consider an example of a simple GET request handler using the @Get() decorator:

@Get('users/:id')
async getUser(@Param('id') id: string): Promise<User> {
  // Logic to fetch user by ID
}
Enter fullscreen mode Exit fullscreen mode

The @Get('users/:id') decorator informs NestJS that this method handles GET requests to the /users/:id route. This declarative approach enhances code readability and simplifies routing logic.

Crafting Custom Decorators

While NestJS offers numerous built-in decorators, creating custom decorators empowers developers to encapsulate specific functionalities or behaviors.

1. Basic Structure

A custom decorator in NestJS is simply a function that returns a decorator factory, which is then applied to the target declaration.

// Decorator factory
export const MyCustomDecorator = (data: string): MethodDecorator => {
  return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
    // Decorator logic goes here
    Reflect.defineMetadata('myCustomData', data, target, propertyKey); 
  };
};
Enter fullscreen mode Exit fullscreen mode

In this example, MyCustomDecorator accepts data as an argument and utilizes Reflect.defineMetadata to attach this data to the decorated method. This metadata can be retrieved later for various purposes.

2. Accessing Metadata

NestJS provides the Reflector class to access metadata attached via decorators.

constructor(private readonly reflector: Reflector) {}

@Get('users/:id')
@MyCustomDecorator('someData')
async getUser(@Param('id') id: string): Promise<User> {
  const customData = this.reflector.get('myCustomData', context.getHandler()); 
  // Utilize customData within the handler logic
}
Enter fullscreen mode Exit fullscreen mode

Unveiling Powerful Use Cases

Custom decorators unlock a realm of possibilities in NestJS applications, promoting code reuse and modularity. Here are some compelling use cases:

1. Role-Based Access Control (RBAC)

Implement fine-grained access control by creating a custom decorator to restrict access to specific roles.

export const Roles = (...roles: Role[]): MethodDecorator => {
  return SetMetadata('roles', roles);
};

@Get('admin')
@Roles('admin') 
async getAdminData() {
  // Only accessible by users with the 'admin' role
}
Enter fullscreen mode Exit fullscreen mode

A global guard can then utilize the Reflector to retrieve allowed roles and enforce authorization.

2. Request/Response Transformation

Streamline data transformation by defining decorators for common manipulations like object sanitization or response formatting.

export const Sanitize = (): MethodDecorator => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      // Sanitize request data here (e.g., using a library like class-transformer)
      const result = await originalMethod.apply(this, args);
      return result;
    };
  };
};

@Post()
@Sanitize()
async createUser(@Body() userData: CreateUserDto) { 
  // userData will be sanitized before further processing
}
Enter fullscreen mode Exit fullscreen mode

3. Input Validation

Enforce data integrity by creating custom decorators for validation logic.

export const ValidateDto = (dtoType: any): MethodDecorator => {
  return usePipes(new ValidationPipe({ transform: true, transformOptions: { targetMap: { dtoType } } }));
};

@Post()
@ValidateDto(CreateUserDto) 
async createUser(@Body() userData: CreateUserDto) { 
  // userData will be automatically validated against the CreateUserDto schema
}
Enter fullscreen mode Exit fullscreen mode

This approach leverages the ValidationPipe and DTO schemas for seamless validation.

4. Caching

Optimize performance by implementing caching mechanisms with custom decorators.

export const CacheResult = (ttl: number): MethodDecorator => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      const cacheKey = JSON.stringify(args);
      const cachedResult = await this.cacheService.get(cacheKey); 
      if (cachedResult) {
        return cachedResult;
      }
      const result = await originalMethod.apply(this, args);
      await this.cacheService.set(cacheKey, result, ttl); 
      return result;
    };
  };
};

@Get()
@CacheResult(60) // Cache for 60 seconds
async getExpensiveData() {
  // Data will be cached based on input arguments
}
Enter fullscreen mode Exit fullscreen mode

5. API Documentation

Enhance API documentation by integrating tools like Swagger using custom decorators to add metadata about endpoints.

export const ApiOperation = (options: ApiOperationOptions): MethodDecorator => {
  return ApiOperation(options); // Utilize the @nestjs/swagger package
};

@Get()
@ApiOperation({ summary: 'Get all users' })
async getUsers() { 
  // API documentation will reflect the provided summary
}
Enter fullscreen mode Exit fullscreen mode

Alternatives and Comparisons

While custom decorators in NestJS offer a powerful mechanism for enhancing APIs, other approaches exist:

  • Middleware: Suitable for logic that needs to be executed before or after a request handler, such as authentication or logging.
  • Pipes: Ideal for transforming input data and validating its structure.
  • Guards: Primarily used for authorization and access control based on specific conditions.

The choice between these options often depends on the specific use case and desired level of granularity.

Conclusion

Custom decorators in NestJS provide a robust and elegant way to enhance the capabilities and maintainability of your APIs. By encapsulating cross-cutting concerns and promoting code reusability, decorators contribute significantly to building scalable and maintainable server-side applications.

Understanding the core principles of decorator creation and exploring various use cases empowers developers to leverage the full potential of custom decorators in NestJS. By embracing decorators, developers can write cleaner, more maintainable code while adhering to the principles of modular design and separation of concerns.

Top comments (0)