- vừa được xem lúc

Setup Boilerplate cho dự án NestJS - Phần 7: Thế nào là một Error Handling hiệu quả

0 0 18

Người đăng: Ngoc Nguyen

Theo Viblo Asia

Đây là bài viết nằm trong Series NestJS thực chiến, các bạn có thể xem toàn bộ bài viết ở link: https://viblo.asia/s/nestjs-thuc-chien-MkNLr3kaVgA


Đặt vấn đề

Error Handling là một chủ đề không mới và luôn luôn hiện diện trong bất kì dự án nào mà chúng ta tham gia. Vì thế việc triển khai như thế nào để hiệu quả và bảo mật là điều chúng ta cần quan tâm. Thông thường chúng ta cần đáp ứng các yêu cầu sau:

  • (1). Recognizable: Phải giúp người sử dụng API phân biệt được lỗi giữa từ client và lỗi từ server:
    • Lỗi từ client sẽ là các lỗi 4xx (unauthenticate, validation error,...): khi đó họ phải chỉnh sửa lại thông tin request.
    • Lỗi từ server là các lỗi 5xx(bad gateway, service unavailable): với các lỗi này người dùng có thể thử gọi lại mà không cần thay đổi gì.
  • (2). Give Context: Lỗi trả về từ API phải bao gồm context để có thể dễ dàng tìm ra nguyên nhân nơi nó phát sinh để giải quyết.
  • (3). Human Readability: (2) là giúp cho BE fix error, còn phía FE chỉ cần một vài thông tin để kiểm soát lỗi nên chúng ta cần làm sao để khi nhìn vào, họ có thể biết được cơ bản lỗi đó là gì.
  • (4). High security: Khi triển khai môi trường production, các thông tin lỗi trả về phải đảm bảo các vấn đề bảo mật. Thường chỉ là dạng dictionary, không được chứa bất cứ thông tin gì về context.

Thông tin package

Các bạn có thể tải về toàn bộ source code của phần này tại đây.

Triển khai

Để bắt đầu chúng ta sẽ viết một GlobalExceptionFilter để override lại default của NestJS, ở đây chúng ta sẽ chuẩn hóa lại các thông tin response để đảm bảo các yêu cầu ở phần trên.

import { ArgumentsHost, Catch, ExceptionFilter, HttpException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Response } from 'express'; @Catch()
export class GlobalExceptionFilter implements ExceptionFilter { constructor(private readonly config_service: ConfigService) {} catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const status = exception instanceof HttpException ? exception.getStatus() : 500; const message = exception instanceof HttpException ? exception.message : 'Internal server error'; response.status(status).json({ statusCode: status, message, error: this.config_service.get('NODE_ENV') !== 'production' ? { response: exception.response, stack: exception.stack, } : null, }); }
}

Giải thích:

  • Để đáp ứng yêu cầu (1) chúng ta sẽ kiểm tra exception catch được có phải là HttpException không. Nếu đúng thì là thường là lỗi từ client, còn lại là của server hoặc các third-party package.
  • response.status(status)... chỗ này có nhiều cách triển khai tùy vào team dev, có team sẽ luôn trả về 200 và chỉ trả về 500 khi lỗi server, có team thì luôn trả về 200 và lỗi thì trả về 400 và họ đều có lý do riêng của mình. Phần này là tùy thuộc vào mỗi người, còn mình không làm cách đó mà sẽ trả đúng status để tránh khi có lỗi phía FE phải check 2 lần mới biết được message lỗi là gì.
  • ...json({ statusCode: status, message, ...}) với các lỗi từ phía client chúng ta sẽ hiển thị ra message, ở phía dưới chúng ta sẽ nói rõ hơn nội dung của message. Còn về phần statusCode thì mình chỉ dùng cho trường hợp FE cần, các bạn có thể bỏ cũng được.
  • Các thông tin về stack trace của lỗi hoặc response chi tiết sẽ được ẩn ở môi trường production (4).

Để sử dụng chúng ta sẽ thêm vào AppModule (do chúng ta có inject ConfigService nên không thể dùng trong file main.ts):

import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './exception-filters/global-exception.filter';
...
@Module({ ... providers: [ AppService, { provide: APP_FILTER, useClass: GlobalExceptionFilter, }, ],
})
export class AppModule {}

Chúng ta đã xong với yêu cầu (1)(4), tiếp theo để làm cho error chúng ta trở nên đầy đủ context hơn mình sẽ tạo ra một dictionary chứa danh sách các lỗi bằng message code để làm ví dụ.

export enum ERRORS_DICTIONARY { // AUTH EMAIL_EXISTED = 'ATH_0091', WRONG_CREDENTIALS = 'ATH_0001', CONTENT_NOT_MATCH = 'ATH_0002', UNAUTHORIZED_EXCEPTION = 'ATH_0011', // TOPIC TOPIC_NOT_FOUND = 'TOP_0041', // USER USER_NOT_FOUND = 'USR_0041', // CLASS VALIDATOR VALIDATION_ERROR = 'CVL_0001',
}

Việc quy định error code có định dạng như thế nào tùy thuộc vào các thành viên trong team thống nhất, làm sao khi nhìn vào các bạn có thể biết ngay lỗi đó thuộc về module nào và loại lỗi là gì. Ví dụ ở đây ATH_0091: ATH = Auth, 009 = 409 và 1 dùng để phân biệt giữa các lỗi 409 của module Auth (conflic email, username,...).

Khi sử dụng chúng ta sẽ kết hợp với các sub-class của HttpException để GlobalExceptionFilter có thể nhận biết đó là instanceof HttpException:

import { ERRORS_DICTIONARY } from 'src/constraints/error-dictionary.constraint';
...
export class AuthService { ... async getAuthenticatedUser(email: string, password: string): Promise<User> { try { ... } catch (error) { throw new BadRequestException({ message: ERRORS_DICTIONARY.WRONG_CREDENTIALS, }); } }

Có thể thấy ở phía môi trường dev, codebase chúng ta sử dụng rất tường minh, khi nhìn vào là biết ngay là lỗi gì - có thể chi tiết hơn nữa WRONG_CREDENTIALS thành WRONG_EMAIL_PASS_CREDENTIALS để phân tách giữa các loại của nó.

Khi đăng nhập với thông tin không hợp lệ kết quả sẽ hiển thị như sau:

Đứng ở phía BE khi nhìn vào response, chúng ta đã có thể biết được lỗi đó nằm ở đâu trong code dựa vào stack trace, kết hợp với error code ATH_0001 chúng ta biết được đó là lỗi do chưa đăng nhập nên không có quyền truy cập. Như vậy chúng ta tiếp tục thỏa mãn được yêu cầu (2).

Tuy nhiên ở phương diện FE, khi nhìn vào response trên họ sẽ không biết ngay lỗi là gì mà phải vào xem chú thích để tra dựa vào error code, việc đó rất bất tiện. Để khắc phục chúng ta cần làm gì đó để cho nó có tính Human Readability hơn, đây cũng là yêu cầu (3).

import { ERRORS_DICTIONARY } from 'src/constraints/error-dictionary.constraint';
...
export class AuthService { ... async getAuthenticatedUser(email: string, password: string): Promise<User> { try { ... } catch (error) { throw new BadRequestException({ message: ERRORS_DICTIONARY.WRONG_CREDENTIALS, details: 'Wrong credentials!!', }); } }

Bằng cách thêm vào property details chúng ta có thể diễn giải được nội dung lỗi chi tiết hơn, khi đó FE hoặc bất kì bên nào sử dụng API của chúng ta của có thể nắm bắt được cơ bản của vấn đề.

Kết quả cuối cùng chúng ta mong đợi như sau:

Nếu ở môi trường production thì sẽ như sau:

Có thể thấy được, việc chuẩn bị Error Handling cho dự án trước khi bắt tay vào code sẽ giúp ít cho chúng ta cũng như người sử dụng API rất nhiều. Hạn chế tối đa được các tình huống khi những thành viên khác trong team sử dụng API gặp lỗi và phải liên hệ cho chúng ta vì không biết được đó là lỗi gì.

Kết luận

Tuy bài viết này khá ngắn gọn nhưng chúng ta đã bao quát được khá nhiều về việc xử lý lỗi trong dự án NestJS và triển khai các giải pháp nhằm đáp ứng các yêu cầu quan trọng như phân biệt lỗi client/server, cung cấp context, đảm bảo đọc được cho người đọc lỗi và bảo mật thông tin. Việc chuẩn bị quy trình xử lý lỗi từ đầu sẽ giúp giảm thiểu các tình huống lỗi và tăng tính ổn định của ứng dụng, đồng thời cung cấp trải nghiệm tốt hơn cho người dùng API.

Hẹn gặp lại các bạn vào các bài viết tiếp theo. Cảm ơn các bạn đã giành thời gian đọc bài viết.

Tài liệu tham khảo

Bình luận

Bài viết tương tự

- vừa được xem lúc

Nơi đẹp nhất chính là nơi phù hợp nhất và câu chuyện giữa Anh Chàng NodeJS và Cô Nàng V8 ?

Kí Ức Đọng Về. Xin chào, lại là mình đây, sau gần 3 tuần vắng bóng với những chồng công việc không hồi kết, hôm nay mình cũng dành ra được thời gian để tiếp tục quay lại với Series NodeJS và những câu chuyện tối ưu Performance.

0 0 29

- vừa được xem lúc

NodeJS có thực sự nhanh như bạn nghĩ? ?

NodeJS dưới ánh mắt người đời. Có nhiều bạn đặt câu hỏi với mình quanh về vấn đề Hiệu Năng của NodeJS, chẳng hạn như:.

0 0 31

- vừa được xem lúc

[Nodejs thực chiến] Dockerize, Containerize nodejs app thật chuẩn

1. Đặt vấn đề:.

0 0 43

- vừa được xem lúc

Thật ngớ ngẩn khi mình dev NodeJs mãi một năm mới biết đến Microtask và Macrotask 💻🐸

Lời nói đầu. -----Xin chào các bạn mình tên là Vinh, hiện tại đang là một lập trình viên Nodejs.

0 0 26

- vừa được xem lúc

[Authentication] Xác thực với JWT cùng mã hóa Đối xứng và Bất đối xứng (Phần 2)

Trong bài trước, chúng ta đã cùng tìm hiểu về các kiến thức cơ bản và cơ chế hoạt động của JWT. Trong bài này, anh em mình sẽ tiếp tục cùng nhau tìm hiểu về một phần mà mình nghĩ là hay nhất khi đề cậ

0 0 26

- vừa được xem lúc

Hiều về kiến trúc hướng sự kiện của Node.js

Bài viết được dịch từ nguồn. Hầu hết các node objects như HTTP request, HTTP response hay HTTP stream - đều implement EventEmitter module nên chúng đều có thể:.

0 0 33