avatarLiu Ting Chun

Summary

The provided web content explains how to create custom parameter decorators in NestJS using the createParamDecorator() function to access and manipulate request attributes such as req.jwt.

Abstract

NestJS leverages decorators extensively for object declaration, configuration, and assignment. The article focuses on the use of custom parameter decorators to access and process custom request attributes like req.jwt, which are not covered by default decorators such as @Session, @Param, @Body, @Query, and @Headers. The createParamDecorator() function is highlighted as a built-in utility for creating these custom decorators. The article illustrates how to use this function to create a @Jwt decorator that can optionally accept a key to access specific properties of req.jwt and includes a discussion on handling multiple options, such as validation and nullability, within a single decorator. The author proposes a solution to encapsulate complex logic within a wrapper function, allowing for cleaner and more maintainable controller methods. Additionally, the article distinguishes the use cases for createParamDecorator(), emphasizing its relevance when the decorator interacts with the request object.

Opinions

  • The author suggests that the default decorators provided by NestJS cover most use cases but may not be sufficient for accessing custom request attributes.
  • A key point conveyed is the importance of being able to pass multiple options to a decorator, which can include validation logic to enhance API security and robustness.
  • The author considers it "ugly" and problematic to create separate decorators for different use cases, advocating instead for a more flexible and maintainable approach.
  • The author's solution to use a wrapper function for handling multiple options is presented as a simpler and cleaner alternative to other methods, such as creating multiple decorators or wrapping keys in objects.
  • The article implies that developers should choose the right tool for the job, using createParamDecorator() when dealing with request-related parameters and resorting to creating decorators from scratch when the functionality is unrelated to request handling.

NestJS — Creating custom parameter decorators for your APIs with createParamDecorator()

If you have played with NestJS for a while, you should have been familiar with decorators. It is because NestJS utilized decorators to do the declaration, configuration and assignment of your objects. You should have seen them everywhere.

Here are five decorators that you may already have been frequently using for creating APIs: @Session, @Param, @Body , @Query and @Headers , which access req.session , req.params , req.body , req.query and req.headers correspondingly.

@Post('create-proposal')
async createProposal(
  @Body('title') title,
  @Body('price') price,
  ...
) {
  ...
}

However, what if you want to access your custom request attributes, such as req.jwt ?

@Post('create-proposal')
async createProposal(
  @Jwt('memberId') memberId,
  @Body('title') title,
  @Body('price') price,
  ...
) {
  ...
}

Custom parameter decorators with a key

Actually, NestJS already has a built-in function for you to create such custom decorators — createParamDecorator()

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

export const Jwt = createParamDecorator((data, req) => {
  return req.jwt;
});

Very simple, isn’t?

In default, the decorator created will accept 1 optional parameter, which is data . One common way to utilize it is to put the attribute key there.

import { createParamDecorator } from '@nestjs/common';
export const Jwt = createParamDecorator((data, req) => {
  let jwt = req['jwt'];
  return jwt && data ? jwt[data] : jwt;
});

As a result, if you put @Jwt() , it will assign the whole req.jwt object to your variable. If there is a key provided, such as @Jwt('memberId') , it will assign req.jwt.memberId instead.

In fact, you can find a similar section at the official document. Hence, I am going to add something that you cannot find from that document in the next section.

Custom parameter decorators with multiple options

The problem here is, having a key can solve most use cases, but what if only a key is not enough? Suppose you want to add validation in your custom parameter decorator — if req.jwt is null, it will throw an exception. However, you want some of the APIs to be able to bypass the validation.

@Post('create-proposal')
async createProposal(
  @Jwt('memberId') memberId,
  @Body('title') title,
  @Body('price') price,
  ...
) {
  ...
}
@Get('get-proposal')
async getProposal(
  @Jwt('memberId', { isNullable: true }) memberId,
  @Query('proposalId') proposalId
) {
  ...
}

The simplest way is to create two different decorators — @Jwt and @JwtNullable, one with validation, one without. It will work perfectly fine, except it is a bit ugly. Also, it will be problematic when you have multiple options.

Another simple way is to wrap the key and all your options into a single object. It is slightly better than the previous one, but still not ideal as you need to wrap the key into an object even if there isn’t any options.

@Post('create-proposal')
async createProposal(
  @Jwt({ key: 'memberId' }) memberId,
  @Body('title') title,
  @Body('price') price,
  ...
) {
  ...
}
@Get('get-proposal')
async getProposal(
  @Jwt({ key: 'memberId', isNullable: true }) memberId,
  @Query('proposalId') proposalId
) {
  ...
}

My solution is actually simpler than you may think. The key point here is that, decorators are in fact, just functions. What you need to do is to create a wrapper function in between.

import { createParamDecorator } from '@nestjs/common';
import { HttpException } from '@nestjs/common/exceptions'
export const Jwt = (key, options: { 
  isNullable?: boolean
} = {}): ParameterDecorator => {
  let validators = [];
  if (!options.isNullable) {
    validators.push(v => {
      if (!v) {
        throw new HttpException({
          statusCode: 401,
          error: "Unauthorized",
          message: "Cannot find a valid JWT token"
        }, 401);
      }
    })
  }
  return createParamDecorator((data, req) => {
    let { key, validators } = data;
    let jwt = req['jwt'];
    
    validators.forEach(validator => { validator(jwt) })
    return jwt && key ? jwt[key] : jwt;
  })({ key, validators });
}

You are creating a function which accepts two parameters — key and options. Instead of directly using the decorator created with createParamDecorator() in your controller, you are now using your custom function as decorator, which helps you to transform your parameters into a single object. As a result, you can encapsulate the ugly part and keep the decorator clean.

When to use createParamDecorator()?

I have written another article about creating decorators from scratch for API validation previously and I didn’t use createParamDecorator() there. Simply speaking, you should use createParamDecorator() only if you are creating parameter decorators, and your decorator is interacting with the req object. If your decorator has nothing to do with the API request, createParamDecorator() will not help.

JavaScript
Nestjs
Nodejs
Typescript
Recommended from ReadMedium