avatarLiu Ting Chun

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

7214

Abstract

import</span> { HttpException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common/exceptions'</span>;</pre></div><div id="1c01"><pre><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Validate</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">return</span> <span class="hljs-keyword">function</span>(<span class="hljs-params">target: any, propertyName: string, descriptor: TypedPropertyDescriptor<<span class="hljs-built_in">Function</span>></span>) { <span class="hljs-keyword">let</span> method = descriptor.<span class="hljs-property">value</span>; descriptor.<span class="hljs-property">value</span> = <span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) { <span class="hljs-keyword">let</span> <span class="hljs-title function_">validate</span> = (<span class="hljs-params">isOptional, parameter</span>) => { <span class="hljs-keyword">let</span> <span class="hljs-title function_">action</span> = (<span class="hljs-params">value</span>) => { <span class="hljs-comment">//*** Your validation start here</span> <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> parameter.<span class="hljs-property">type</span> === <span class="hljs-string">'string'</span>) { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> value !== parameter.<span class="hljs-property">type</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>({ <span class="hljs-attr">statusCode</span>: <span class="hljs-number">422</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unprocessable Entity'</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">'One/some parameter(s) is/are with incorrect type (index code: '</span> + parameter.<span class="hljs-property">index</span> + <span class="hljs-string">', expected type: '</span> + parameter.<span class="hljs-property">type</span> + <span class="hljs-string">', received: '</span> + <span class="hljs-keyword">typeof</span> value + <span class="hljs-string">')'</span> }, <span class="hljs-number">422</span>); } <span class="hljs-keyword">switch</span>(parameter.<span class="hljs-property">type</span>) { <span class="hljs-keyword">case</span> <span class="hljs-string">'number'</span>: { <span class="hljs-keyword">if</span> ((!isOptional || value != <span class="hljs-literal">null</span>) && parameter.<span class="hljs-property">config</span>.<span class="hljs-property">max</span> != <span class="hljs-literal">null</span> && value > parameter.<span class="hljs-property">config</span>.<span class="hljs-property">max</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>({ <span class="hljs-attr">statusCode</span>: <span class="hljs-number">422</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unprocessable Entity'</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">'One/some parameter(s) is/are too large (index code: '</span> + parameter.<span class="hljs-property">index</span> + <span class="hljs-string">', maximun: '</span> + parameter.<span class="hljs-property">config</span>.<span class="hljs-property">max</span> + <span class="hljs-string">', received: '</span> + value + <span class="hljs-string">')'</span> }, <span class="hljs-number">422</span>); } <span class="hljs-keyword">if</span> ((!isOptional || value != <span class="hljs-literal">null</span>) && parameter.<span class="hljs-property">config</span>.<span class="hljs-property">min</span> != <span class="hljs-literal">null</span> && value < parameter.<span class="hljs-property">config</span>.<span class="hljs-property">min</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>({ <span class="hljs-attr">statusCode</span>: <span class="hljs-number">422</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unprocessable Entity'</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">'One/some parameter(s) is/are too small (index code: '</span> + parameter.<span class="hljs-property">index</span> + <span class="hljs-string">', minimum: '</span> + parameter.<span class="hljs-property">config</span>.<span class="hljs-property">min</span> + <span class="hljs-string">', received: '</span> + value + <span class="hljs-string">')'</span> }, <span class="hljs-number">422</span>); } <span class="hljs-keyword">break</span>; } <span class="hljs-keyword">case</span> <span class="hljs-string">'string'</span>: { <span class="hljs-keyword">let</span> length = value ? value.<span class="hljs-property">length</span> : <span class="hljs-number">0</span>; <span class="hljs-keyword">if</span> ((!isOptional || value != <span class="hljs-literal">null</span>) && parameter.<span class="hljs-property">config</span>.<span class="hljs-property">max</span> != <span class="hljs-literal">null</span> && length > parameter.<span class="hljs-property">config</span>.<span class="hljs-property">max</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>({ <span class="hljs-attr">statusCode</span>: <span class="hljs-number">422</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unprocessable Entity'</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">'One/some parameter(s) is/are too long (index code: '</span> + parameter.<span class="hljs-property">index</span> + <span class="hljs-string">', maximun: '</span> + parameter.<span class="hljs-property">config</span>.<span class="hljs-property">max</span> + <span class="hljs-string">', received length: '</span> + length + <span class="hljs-string">')'</span> }, <span class="hljs-number">422</span>); } <span class="hljs-keyword">if</span> ((!isOptional || value != <span class="hljs-literal">null</span>) && parameter.<span class="hljs-property">config</span>.<span class="hljs-property">min</span> != <span class="hljs-literal">null</span> && length < parameter.<span class="hljs-property">config</span>.<span class="hljs-property">min</span>) { <span class="hljs-keyword">throw</span> <span class="hljs

Options

-keyword">new</span> <span class="hljs-title class_">HttpException</span>({ <span class="hljs-attr">statusCode</span>: <span class="hljs-number">422</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unprocessable Entity'</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">'One/some parameter(s) is/are too short (index code: '</span> + parameter.<span class="hljs-property">index</span> + <span class="hljs-string">', minimum: '</span> + parameter.<span class="hljs-property">config</span>.<span class="hljs-property">min</span> + <span class="hljs-string">', received length: '</span> + length + <span class="hljs-string">')'</span> }, <span class="hljs-number">422</span>); } <span class="hljs-keyword">break</span>; } <span class="hljs-comment">/* ** All available type: boolean, number, string, object, function, undefined /</span> <span class="hljs-attr">default</span>: {} } } <span class="hljs-comment">/ ** Validate enum (Object) /</span> <span class="hljs-keyword">else</span> { <span class="hljs-keyword">let</span> valid = <span class="hljs-literal">false</span>; <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> k <span class="hljs-keyword">in</span> parameter.<span class="hljs-property">type</span>) { <span class="hljs-keyword">if</span> (parameter.<span class="hljs-property">type</span>[k] === value) { valid = <span class="hljs-literal">true</span>; <span class="hljs-keyword">break</span>; } } <span class="hljs-keyword">if</span> (!valid) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>({ <span class="hljs-attr">statusCode</span>: <span class="hljs-number">422</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unprocessable Entity'</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">'One/some parameter(s) is/are with incorrect value (index code: '</span> + parameter.<span class="hljs-property">index</span> + <span class="hljs-string">', expected value: ['</span> + <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">keys</span>(parameter.<span class="hljs-property">type</span>).<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">key</span> =></span> parameter.<span class="hljs-property">type</span>[key]) + <span class="hljs-string">'], received: '</span> + value + <span class="hljs-string">')'</span> }, <span class="hljs-number">422</span>); } <span class="hljs-comment">//** Your validation end here</span> } } <span class="hljs-title function_">action</span>(<span class="hljs-variable language_">arguments</span>[parameter.<span class="hljs-property">index</span>]); } <span class="hljs-keyword">let</span> requiredParameters = <span class="hljs-title class_">Reflect</span>.<span class="hljs-title function_">getOwnMetadata</span>(requiredMetadataKey, target, propertyName); <span class="hljs-keyword">if</span> (requiredParameters) { <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> parameter <span class="hljs-keyword">of</span> requiredParameters) { <span class="hljs-keyword">if</span> (parameter.<span class="hljs-property">index</span> >= <span class="hljs-variable language_">arguments</span>.<span class="hljs-property">length</span> || <span class="hljs-variable language_">arguments</span>[parameter.<span class="hljs-property">index</span>] == <span class="hljs-literal">null</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>({ <span class="hljs-attr">statusCode</span>: <span class="hljs-number">422</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unprocessable Entity'</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">'One/some parameter(s) is/are missing (index code: '</span> + parameter.<span class="hljs-property">index</span> + <span class="hljs-string">')'</span> }, <span class="hljs-number">422</span>); } <span class="hljs-keyword">if</span> (parameter.<span class="hljs-property">type</span> != <span class="hljs-literal">null</span>) { <span class="hljs-title function_">validate</span>(<span class="hljs-literal">false</span>, parameter); } } } <span class="hljs-keyword">let</span> optionalParameters = <span class="hljs-title class_">Reflect</span>.<span class="hljs-title function_">getOwnMetadata</span>(optionalMetadataKey, target, propertyName); <span class="hljs-keyword">if</span> (optionalParameters) { <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> parameter <span class="hljs-keyword">of</span> optionalParameters) { <span class="hljs-keyword">if</span> (parameter.<span class="hljs-property">type</span> != <span class="hljs-literal">null</span> && <span class="hljs-variable language_">arguments</span>[parameter.<span class="hljs-property">index</span>] != <span class="hljs-literal">null</span>) { <span class="hljs-title function_">validate</span>(<span class="hljs-literal">true</span>, parameter); } } } <span class="hljs-keyword">return</span> method.<span class="hljs-title function_">apply</span>(<span class="hljs-variable language_">this</span>, <span class="hljs-variable language_">arguments</span>); } } }</pre></div><p id="65af">The method decorator is where the validation logic really in. The code above defines a <code>@Validate()</code> decorator to place on our controller methods. It will read all the metadata supplied by our parameter decorators, to determine how each of our parameters should be validated.</p><p id="4153">For this example, it will first read all the parameters with <code>@Required()</code> to do the null checking. After that, it passes all the <code>@Required()</code> and <code>@Optional()</code> with type defined to do the type checking.</p><p id="25ad">The above example covers only the validation of enum and primitive data types. Yet, you may sometimes have an object in your parameters. It is possible to also validate objects with custom decorators. However, I won’t recommend doing that as the object structure is usually very API-specific. It is difficult to have a generic way to describe those parameters. I suggest you to keep your decorator simple and do the specific object validation in your controller. Finally, hope this article is useful for you!</p></article></body>

NestJS — Validate requests with custom decorators

It is a must to add validation to all your APIs to avoid unexpected usage. The official document of NestJS suggests to validate requests with the Validation Pipe. Yet, the drawback is you will have to create a DTO for each API. If you don’t like the official approach, you may try my custom decorator approach. Imagine something like this:

@Post('create-proposal')
@Validate()
createProposal(
  @Body('title') @Required('string', { max: 36 }) title: string,
  @Body('type') @Required(ProposalType) type: ProposalType,
  @Body('price') @Required('number', { min: 0 }) price: number,
  @Body('remark') @Optional('string', { max: 2000 }) remark: string
) {
  ...
}

This approach is much simpler and more convenient when you are working on a small scale application.

For simplicity, this article will only cover the basic use cases, which are enum validation and some of the primitive type validations.

Introduction & Setup

We are going to define two type of decorators. First, some parameter level decorators for us to add metadata to our parameters. In simple, to tell the system which parameter to validate and how it should be validated. Secondly, we will define a method level decorator which holds all the validation logic. It will read all the metadata in run-time and run the validation based on the metadata.

First things first, we need to install the reflect-metadata package which helps us to declare and read metadata.

npm install reflect-metadata

Define parameter decorators

const requiredMetadataKey = Symbol('required');
const optionalMetadataKey = Symbol('optional');
export function Optional(type?: string | object, config?: {
  max?: number,
  min?: number
}) {
  return function (
    target: object, 
    propertyKey: string | symbol, 
    parameterIndex: number
  ) {
    let existingOptionalParameters = Reflect.getOwnMetadata(optionalMetadataKey, target, propertyKey) || [];
    existingOptionalParameters.push({
      index: parameterIndex,
      type: type,
      config: config || {}
    });
    Reflect.defineMetadata(optionalMetadataKey, existingOptionalParameters, target, propertyKey);
  }
}
export function Required(type?: string | object, config?: {
  max?: number,
  min?: number
}) {
  return function (
    target: object, 
    propertyKey: string | symbol, 
    parameterIndex: number
  ) {
    let existingRequiredParameters = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push({
      index: parameterIndex,
      type: type,
      config: config || {}
    });
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
  }
}

Here are examples to define two parameter decorators, @Required() and @Optional() . @Required() declares that the parameter is mandatory, while @Optional() for nullable. We can further state the data type and max/min value within the decorators. You may extend the config parameter to suit more cases, like isEmail, etc. In case you want to combine two decorators into one, you may also add isNullable in your config. One thing to keep in mind is parameter decorator should not include any validation logic. It is only used to describe your parameter. All your validations are done in next section.

Define method decorators

import 'reflect-metadata';
import { HttpException } from '@nestjs/common/exceptions';
export function Validate() {
  return function(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
      let validate = (isOptional, parameter) => {
        let action = (value) => {
          //*** Your validation start here
          if (typeof parameter.type === 'string') {
            if (typeof value !== parameter.type) {
              throw new HttpException({
                statusCode: 422,
                error: 'Unprocessable Entity',
                message: 'One/some parameter(s) is/are with incorrect type (index code: ' + parameter.index + ', expected type: ' + parameter.type + ', received: ' + typeof value + ')'
              }, 422);
            }
            switch(parameter.type) {
              case 'number': {
                if ((!isOptional || value != null) && parameter.config.max != null && value > parameter.config.max) {
                  throw new HttpException({
                    statusCode: 422,
                    error: 'Unprocessable Entity',
                    message: 'One/some parameter(s) is/are too large (index code: ' + parameter.index + ', maximun: ' + parameter.config.max + ', received: ' + value + ')'
                  }, 422);
                }
                if ((!isOptional || value != null) && parameter.config.min != null && value < parameter.config.min) {
                  throw new HttpException({
                    statusCode: 422,
                    error: 'Unprocessable Entity',
                    message: 'One/some parameter(s) is/are too small (index code: ' + parameter.index + ', minimum: ' + parameter.config.min + ', received: ' + value + ')'
                  }, 422);
                }
                break;
              }
              case 'string': {
                let length = value ? value.length : 0;
                if ((!isOptional || value != null) && parameter.config.max != null && length > parameter.config.max) {
                  throw new HttpException({
                    statusCode: 422,
                    error: 'Unprocessable Entity',
                    message: 'One/some parameter(s) is/are too long (index code: ' + parameter.index + ', maximun: ' + parameter.config.max + ', received length: ' + length + ')'
                  }, 422);
                }
                if ((!isOptional || value != null) && parameter.config.min != null && length < parameter.config.min) {
                  throw new HttpException({
                    statusCode: 422,
                    error: 'Unprocessable Entity',
                    message: 'One/some parameter(s) is/are too short (index code: ' + parameter.index + ', minimum: ' + parameter.config.min + ', received length: ' + length + ')'
                  }, 422);
                }
                break;
              }
              /* 
              ** All available type: boolean, number, string, object, function, undefined
              */
              default: {}
            }
          }
          /*
          ** Validate enum (Object)
          */
          else {
            let valid = false;
            for (let k in parameter.type) {
              if (parameter.type[k] === value) {
                valid = true;
                break;
              }
            }
            if (!valid) {
              throw new HttpException({
                statusCode: 422,
                error: 'Unprocessable Entity',
                message: 'One/some parameter(s) is/are with incorrect value (index code: ' + parameter.index + ', expected value: [' + Object.keys(parameter.type).map(key => parameter.type[key]) + '], received: ' + value + ')'
              }, 422);
            }
            //*** Your validation end here
          }
        }
        action(arguments[parameter.index]);
      }
      let requiredParameters = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
      if (requiredParameters) {
        for (let parameter of requiredParameters) {
          if (parameter.index >= arguments.length || arguments[parameter.index] == null) {
            throw new HttpException({
              statusCode: 422,
              error: 'Unprocessable Entity',
              message: 'One/some parameter(s) is/are missing (index code: ' + parameter.index + ')'
            }, 422);
          }
          if (parameter.type != null) {
            validate(false, parameter);
          }
        }
      }
      let optionalParameters = Reflect.getOwnMetadata(optionalMetadataKey, target, propertyName);
      if (optionalParameters) {
        for (let parameter of optionalParameters) {
          if (parameter.type != null && arguments[parameter.index] != null) {
            validate(true, parameter);
          }
        }
      }
      return method.apply(this, arguments);
    }
  }
}

The method decorator is where the validation logic really in. The code above defines a @Validate() decorator to place on our controller methods. It will read all the metadata supplied by our parameter decorators, to determine how each of our parameters should be validated.

For this example, it will first read all the parameters with @Required() to do the null checking. After that, it passes all the @Required() and @Optional() with type defined to do the type checking.

The above example covers only the validation of enum and primitive data types. Yet, you may sometimes have an object in your parameters. It is possible to also validate objects with custom decorators. However, I won’t recommend doing that as the object structure is usually very API-specific. It is difficult to have a generic way to describe those parameters. I suggest you to keep your decorator simple and do the specific object validation in your controller. Finally, hope this article is useful for you!

JavaScript
Nestjs
Recommended from ReadMedium