NestJS API 05. Error handling and data validation


NestJS nổi bật khi nhắc đến việc error handling và data validation. Những điều đó nhờ vào việc sử dụng decorators. Trong bài hôm nay, chúng ta cùng nhau tìm hiểu các tính năng mà NestJS cung cấp cho chúng ta, ví dụ như Exception filters và Validation pipes.

Theo dõi source code tại đây, branch dev nha

Link postman để test cho dễ.

Exception filters

Nest có một bộ lọc ngoại lệ giúp xử lý các lỗi trong ứng dụng của chúng ta. Bất cứ khi nào chúng ta không tự xử lý một ngoại lệ, bộ lọc ngoại lệ sẽ làm điều đó cho chúng ta. Nó xử lý ngoại lệ và gửi nó trong response ở định dạng user-friendly.

Mặc định bộ lọc ngoại lệ gọi đến class BaseExceptionFilter. Chúng ta có thể xem bên trong source code và xem nó hoặc động như thế nào:
nest/packages/core/exceptions/base-exception-filter.ts

export class BaseExceptionFilter<T = any> implements ExceptionFilter<T> {
  // ...
  catch(exception: T, host: ArgumentsHost) {
    // ...
    if (!(exception instanceof HttpException)) {
      return this.handleUnknownError(exception, host, applicationRef);
    }
    const res = exception.getResponse();
    const message = isObject(res)
      ? res
      : {
          statusCode: exception.getStatus(),
          message: res,
        };
    // ...
  }
 
  public handleUnknownError(
    exception: T,
    host: ArgumentsHost,
    applicationRef: AbstractHttpAdapter | HttpServer,
  ) {
    const body = {
      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
      message: MESSAGES.UNKNOWN_EXCEPTION_MESSAGE,
    };
    // ...
  }
}

xem cho biết vậy thôi chứ chúng ta không được can thiệp vào các file nằm bên trong folder node_modules này đâu nha.

Mỗi khi có lỗi sinh ra, phương thức catch này sẽ được thực thi. Có một số thông tin cần thiết sẽ được trả về ở đây. Ví dụ như statusCode, message.

HttpException

NestJS khuyến khích chúng ta sử dụng class HttpException. Nếu không sử dụng class này, hệ thống sẽ hiểu rằng đã xảy ra sự cố và phản hồi 500 Internal Server Error.

Ở những bài trước mình cũng dùng rất nhiều thằng này trong service.

throw new HttpException('Post not found', HttpStatus.NOT_FOUND);

Bắt buộc khi khởi tạo phải truyền 2 tham số vào: phần message và status code. Đối với statusCode ta có thể sử dụng HttpStatus đã định nghĩa sẵn giúp ta dễ dàng sử dụng trong từng trường hợp.


thật ra truyền số 6 là nó sẽ lỗi đấy các bạn, phải truyền cái id gì đó của collections nào đó bất kì vào với không lỗi (62713439e262d10be2ffcc3e).

Ngoài cách sử dụng HttpException cho nestjs cung cấp, chúng ta hoàn toàn có thể tạo ra những HttpException do chính chúng ta định nghĩa ra, nhưng mà cũng nên kế thừa từ những gì có sẵn nha, chớ kẻo nó không có chạy đâu á.

post/exceptions/postNotFund.exception.ts

import { HttpException, HttpStatus } from '@nestjs/common';

class PostNotFoundException extends HttpException {
  constructor(postId: number) {
    super(`Post with id ${postId} not found`, HttpStatus.NOT_FOUND);
  }
}

Class PostNotFoundException chúng ta vừa tạo gọi đến hàm constructor của HttpException. Do đó, chúng ta có thể xóa bỏ code này và không xác định thông báo mỗi khi muốn tạo ra throw an error.

NestJS đã cung cấp cho chúng ta nhiều loại Exception tương ứng với những statusCode khác nhau. 1 trong số chúng có NotFoundException. Tìm hiểu thêm tại đây

{
    "statusCode": 404,
    "message": "Post with id 62713439e262d10be2ffcc3e not found",
    "error": "Not Found"
}

Extending the BaseExceptionFilter

Mặc định BaseExceptionFilter có thể handle hầu hết tất cả các trường hợp thông thường. Tuy nhiên, chúng ta vẫn có thể modify nó. Người Việt mình hay có cái tính “Sử dụng của người ta nhưng biến nó thành của mình”, phải sửa chút chút mới thích (nói vui vậy thôi các bạn đừng ném đá).

utils/exceptionLogger.filter.ts

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
 
@Catch()
export class ExceptionLoggerFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    console.log('Exception thrown', exception);
    super.catch(exception, host);
  }
}

@Catch() decorator để bắt tất cả exceptions.

Và chúng ta cần khai báo để có thể thằng NestJS hiểu được phải chạy vô cái file này. Để sử dụng bộ lọc này, có 3 cách khai báo.

Đầu tiên khai báo global, sử dụng app.useGlobalFilters:
main.ts

import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ExceptionLoggerFilter } from './utils/exceptionLogger.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new ExceptionLoggerFilter(httpAdapter));

  await app.listen(3000);
}
bootstrap();

Một cách khai báo global tốt hơn là khai báo vào AppModule

import { Module } from '@nestjs/common';
import { ExceptionLoggerFilter } from './utils/exceptionsLogger.filter';
import { APP_FILTER } from '@nestjs/core';
 
@Module({
  // ...
  providers: [
    {
      provide: APP_FILTER,
      useClass: ExceptionLoggerFilter,
    },
  ],
})
export class AppModule {}

Cách cuối cùng là bind trực tiếp vào trong controller bằng @UseFilters() decorator.

post.controller.ts

@Get(':id')
@UseFilters(ExceptionLoggerFilter)
async getPostById(@Param('id') id: string) {
    return await this.postService.getPostById(id);
}

Đây không phải là cách Log lại các exception tốt nhất. Tùy vào từng mục đích và nhu cầu của các bạn mà sử dụng cho phù hợp. NestJS đã xây dựng Logger rất tốt, mình sẽ nói đến trong series lần này. Bài nào thì chưa biết nữa.

Implementing the ExceptionFilter interface

Chúng ta hoàn toàn có thể thay đổi cấu trúc của một ExceptionFilter bằng cách sau:

import { ExceptionFilter, Catch, ArgumentsHost, NotFoundException } from '@nestjs/common';
import { Request, Response } from 'express';
 
@Catch(NotFoundException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: NotFoundException, host: ArgumentsHost) {
    const context = host.switchToHttp();
    const response = context.getResponse<Response>();
    const request = context.getRequest<Request>();
    const request = context.getRequest<Request>();
    const status = exception.getStatus();
    const message = exception.getMessage();
 
    response
      .status(status)
      .json({
        message,
        statusCode: status,
        time: new Date().toISOString(),
      });
  }
}

Và khai báo theo những cách phía trên mình đã khai báo.

Lưu ý: Khi chúng ta sử dụng @Catch(NotFoundException) nên HttpExceptionFilter chỉ chạy cho mỗi NotFoundException.

Phương thức host.switchToHttp trả về object HttpArgumentsHost với thông tin của HTTP context. Chúng ta sẽ tìm hiểu nó ở những bài sau. Bạn cũng có thể xem qua trước Tại đây.

Validation

Ở những bài trước, mình có sử dụng thư viện class-validator để validator. NestJS cũng đã tích hợp nó vào luôn rồi.

Nhưng sự thật đối với mình thì mình vẫn sử dụng nhiều cái thư viện trên, hehe.

Pipes thường được sử dụng để chuyển đổi dữ liệu đầu vào hoặc xác nhận nó. Hôm nay chúng ta chỉ sử dụng Pipes được xác định trước, nhưng trong các phần sắp tới của loạt bài này, chúng ta có thể xem xét việc custom Pipes.

Để bắt đầu, chúng ta cần sử dụng ValidationPipe.
main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Nói là tích hợp, nhưng phải cài thư viện thì mới hiểu nha:

npm install class-validator class-transformer

Nếu không cài thư viện class-transformer sẽ báo lỗi

[Nest] ERROR [PackageLoader] The “class-transformer” package is missing. Please, make sure to install this library ($ npm install class-transformer) to take advantage of ValidationPipe.

Các bạn check lại bài trước, phần đăng ký, đăng nhập. Khi chưa khai báo ValidationPipe vào main.ts, nếu các bạn gửi thiếu field nào mà được khai báo @IsNotEmpty() thì sẽ nhận về kết quả:

{
    "statusCode": 500,
    "message": "Internal server error"
}

Sau khi khai báo:

{
    "statusCode": 400,
    "message": [
        "password should not be empty"
    ],
    "error": "Bad Request"
}

Có rất nhiều loại validate, các bạn có thể vào trang chủ của nó để xem thêm [tại đây](https://github.com/typestack/class-validator, và cũng có thể custom validate tham khảo thêm tại đây.

Ví dụ về custom validate mình đã dùng:
user.dto.ts

import {
  IsEmail,
  IsNotEmpty,
  registerDecorator,
  ValidationArguments,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from 'class-validator';

export function Match(property: string, validationOptions?: ValidationOptions) {
  return (object: any, propertyName: string) => {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [property],
      validator: MatchConstraint,
    });
  };
}

@ValidatorConstraint({ name: 'Match' })
export class MatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const [relatedPropertyName] = args.constraints;
    const relatedValue = (args.object as any)[relatedPropertyName];
    return value === relatedValue;
  }
}

export class PasswordResetAuthDto {
  @IsNotEmpty() token: string;
  @IsNotEmpty() @IsEmail() readonly email: string;
  @IsNotEmpty() new_password: string;
  @IsNotEmpty() @Match('new_password') confirm_password: string;
}

Cái này để các bạn tham khảo thêm thôi nha.

Validating params

Thằng @Body(), @Query() hay @Param đều validate như nhau.

Thêm code bên dưới vào file post/dto/post.dto.ts

...
export class FindPostDto {
  @IsMongoId() id;
}

Cập nhật lại file post.controller.ts một xíu:

@Get(':id')
  async getPostById(@Param() { id }: FindPostDto) {
    return await this.postService.getPostById(id);
  }

Lưu ý: chúng ta không sử dụng @Param ('id') ở đây nữa. Đối với Body và Query cũng vậy.

Vì mình dùng thằng Nestjs kết nối đến Mongodb nên mình sử dụng @IsMongoId(), các bạn có thể thay vào bất kì gì tùy thích sao cho phù hợp là được.

Handling PATCH

Bài nay hơi ngắn, sẵn tiện chúng ta tìm hiểu về 2 phương thức PUTPATCH.

Vì bình thường mình cũng không đụng tới thằng PATCH này mấy, cứ PUT mà dùng do nó thân thuộc.

Sự khác nhau cơ bản chỉ 1 dòng là nói lên được: PUT là thay đổi 1 entity, trông khi PATCH chỉ thay đổi 1 phần. Khi thay đổi từng phần đó, chúng ta có thể skip qua một số thuộc tính.

Cách đơn giản nhất để xử lý PATCH là set thêm skipMissingProperties vào bên trong hàm khởi tạo:
main.ts

app.useGlobalPipes(new ValidationPipe({ skipMissingProperties: true }));

Thật không may, điều này lại skip qua các thuộc tính bị thiếu trong tất cả DTOs. Khi post data chúng ta không muốn điều đó xảy ra. Thay vào đó, chúng ta cập nhật thêm @IsOptional trong tất cả thuộc tính khi cập nhật dữ liệu.

import { IsString, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
 
export class UpdatePostDto {
  @IsNumber()
  @IsOptional()
  id: number;
 
  @IsString()
  @IsNotEmpty()
  @IsOptional()
  content: string;
 
  @IsString()
  @IsNotEmpty()
  @IsOptional()
  title: string;
}

Giải pháp trên không được tối ưu cho lắm, hihi. Bạn có thể tham khảo thêm một số cách khác tại đây

Như các bạn đã thấy đó, và đó là lí do vì sao mình toàn dùng Put chứ hiếm khi gần như là không dùng patch.

Kết luận

Qua bài viết này, mình đã giới thiệu cũng như hướng dẫn mọi người cách hoặc động cũng như là làm việc với Exception và validation. Một số kiến thức này ít được thông dụng, nhưng đôi khi chúng ta vẫn cần đến phải không nào!

Vẫn còn rất nhiều điều chưa thể viết hết trong bài này, vậy nên mọi người chú ý theo dõi bài viết tiếp theo nha.

Nguồn tham khảo:

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!


Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Nguyễn Quang Đạt !
Comments
  TOC