preface

After the previous article on how to build a logging system using middleware, interceptors, and filters, I’ll look at the pain that the back end never gets around: parameter validation.

Have you ever written a bunch of if-else just to validate arguments? And then you have to judge the various parameter types, right? Similar structures are judged in different ways, but you have to copy the code again?

DTO provides a clear view of the structure of the object, Pipes and class-Validator can be used to check parameter types, and error messages can be thrown if validation fails.

A few days ago, I found that NestJS has been updated to 7.0.3 (previously 6.0.0). In order to make the tutorial more realistic, I decided to update it. After the upgrade, no major problems were found, the previous code is running as usual, if any readers found other bugs, you can issue on GitHub.

GitHub project address, welcome everyone Star.

What is DTO?

Data Transfer Object (DTO) is a software application system that transfers Data between design modes. The data transfer target is usually a data access object that retrieves data from a database. The difference between a data transfer object and a data interaction object or a data access object is one that has no behavior other than storing and retrieving data (accessors and accessors).

User.to. ts = user.to. ts = user.to. ts = user.to. ts = user.to. ts

// src/logical/user
export class RegisterInfoDTO {
  readonly accountName: string | number;
  readonly realName: string;
  readonly password: string;
  readonly repassword: string;
  readonly mobile: number;
}
Copy the code

It outputs a class similar to the declared interface, indicating the parameter names and types, and is read-only.

Of course, Nest supports using interfaces to define Dtos. The syntax can be found in the official TypeScript documentation, but Nest recommends using Class for Dtos. (For the sake of experience, Class is really more convenient than Interface, so Interface is not covered here.

With the DTOS defined, I’ll show you how to work with the pipe to validate the parameters.

Second, the pipeline

1. The concept

Pipes are a bit like interceptors in that they are “checkpoints” in the data transfer process, but each has its own role to play.

There are two types of pipes:

  • Conversion: The pipe converts the input data into the desired data output;
  • Validation: Validates the input data. If the validation succeeds, an exception will be thrown.

ValidationPipe is one of three out-of-box pipes that come with Nest.js (the other two are ParseIntPipe and ParseUUIDPipe, which are not available yet).

The ValidationPipe accepts only one value and immediately returns the same value. It behaves like an identity function. The standard code is as follows:

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable(a)export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    returnvalue; }}Copy the code

Each pipe must provide a transform() method. This method takes two arguments:

  • value
  • metadata

Value is the parameter being processed, and metadata is its metadata.

2. Create a pipe

After a brief introduction to some of the concepts, start with the pipe file:

$ nest g pipe validation pipe
Copy the code

Here we also need to install two dependency packages:

$ yarn add class-validator class-transformer -S
Copy the code

Then write the validation logic in validation.pipe.ts:

// src/pipe/validation.pipe.ts
import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { Logger } from '.. /utils/log4js';

@Injectable(a)export class ValidationPipe implements PipeTransform {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    console.log(`value:`, value, 'metatype: ', metatype);
    if(! metatype || !this.toValidate(metatype)) {
      // If no validation rule is passed in, data is returned without validation
      return value;
    }
    // Convert the object to Class for verification
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      const msg = Object.values(errors[0].constraints)[0]; // Just take the first error message and return it
      Logger.error(`Validation failed: ${msg}`);
      throw new BadRequestException(`Validation failed: ${msg}`);
    }
    return value;
  }

  private toValidate(metatype: any) :boolean {
    const types: any[] = [String.Boolean.Number.Array.Object];
    return !types.includes(metatype);
  }
}
Copy the code

3. Bind pipes

Binding the pipe is very simple, just like using Guards before, bind it directly to the Controller with the modifier, and then specify the body type DTO:

// src/logical/user/user.controller.ts
import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '.. /auth/auth.service';
import { UserService } from './user.service';
import { ValidationPipe } from '.. /.. /pipe/validation.pipe';
import { RegisterInfoDTO } from './user.dto'; / / into the DTO

@Controller('user')
export class UserController {
  constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}

  // JWT authentication -step 1: user requests login
  @Post('login')
  async login(@Body() loginParmas: any) {... }@UseGuards(AuthGuard('jwt'))
  @UsePipes(new ValidationPipe()) // Use pipe validation
  @Post('register')
  async register(@Body() body: RegisterInfoDTO) { // Specify the DTO type
    return await this.usersService.register(body); }}Copy the code

4. Improve error prompts

This is not enough, we should add error message:

// src/logical/user/user.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';

export class RegisterInfoDTO {
  @IsNotEmpty({ message: 'User name cannot be empty' })
  readonly accountName: string | number;
  @IsNotEmpty({ message: 'Real name cannot be empty' })
  @IsString({ message: 'Real name must be String' })
  readonly realName: string;
  @IsNotEmpty({ message: 'Password cannot be empty' })
  readonly password: string;
  @IsNotEmpty({ message: 'Duplicate password cannot be empty' })
  readonly repassword: string;
  @IsNotEmpty({ message: 'Mobile phone number cannot be empty' })
  @IsNumber()
  readonly mobile: number; readonly role? :string | number;
}
Copy the code

GitHub: Class-Validator validator: GitHub: Class-Validator validator: GitHub: Class-Validator

Let’s test the null case first:

In the figure above, you can see that @isnotempty () of accountName is in effect

Note that class-Validator also provides a method called @isEmpty (), which means that the argument must be empty, so don’t get confused.

Postman’s Body -> x-www-form-urlencoded is coded as strings by default, so we’ll need to modify the request parameters slightly:

Realname @isString () is now in effect.

At this point, input parameter validation is almost complete, and with this, we can get rid of all the if-else validations (although special, logically complex ones are still needed).

conclusion

This article explained how to define dtos, how to use Pipes, and how to work with a Class-Validator for entry validation.

Defining DTO might be a bit of a hassle for some, but TypeScript becomes AnyScript instead of any…

If you’re not embracing TypeScript, you might as well write in JavaScript, which is faster (Koa, Egg, etc.). Defining dtos also has the benefit of automatically generating documents with Swagger and being request able, which makes it easier for the front-end to read documents. Future tutorials will explain how to do this.

In the next article, I’ll show you how to use interceptors for permission authentication.

This article is included in the NestJS practical tutorial, more articles please pay attention to.

`