0
0
Lập trình
Admin Team
Admin Teamtechmely

Lọc Nâng Cao với NestJS: Cách Thực Hiện Dễ Dàng

Đăng vào 7 tháng trước

• 6 phút đọc

Lọc Nâng Cao với NestJS: Cách Thực Hiện Dễ Dàng

Khi xây dựng một API, bạn thường cần xử lý các tham số truy vấn phức tạp. Nếu bạn chỉ lập trình một cách giản đơn, điều này có thể không phải là vấn đề lớn, nhưng khi phát triển một giải pháp thực tế, phân trang, lọc và sắp xếp là điều cần thiết để tạo ra một API mạnh mẽ và có thể mở rộng.

Trong dự án hiện tại của tôi với NestJS, tôi đã phải đối mặt với thách thức này. Ban đầu, tôi nghĩ rằng "Dễ thôi, tôi chỉ cần viết các DTO" - nhưng không đơn giản như vậy. Hôm nay, tôi sẽ hướng dẫn bạn cách thực hiện lọc phức tạp trong một endpoint của NestJS.

Tạo Dự Án NestJS

Đầu tiên, chúng ta cần tạo một dự án Nest mới. Nếu bạn đã cài đặt CLI, đây là một bước đơn giản. Nếu chưa, tôi khuyên bạn nên cài đặt nó.

Copy
nest new complex-query-filters

Tiếp theo, vì chúng ta sẽ cần xác thực và chuyển đổi các DTO, hãy cài đặt các gói class-validatorclass-transformer.

Copy
npm i class-validator class-transformer

Vậy là bạn đã có một API NestJS tối thiểu. Tuy nhiên, để xử lý các truy vấn phức tạp, chúng ta cần thực hiện một thay đổi nhỏ trong dự án: khai báo ứng dụng dưới dạng NestExpressApplication. Bạn có thể hỏi, "Đó không phải là một ứng dụng Express theo định nghĩa sao?" Và bạn đúng, nhưng việc khai báo rõ ràng loại ứng dụng cho chúng ta quyền truy cập vào các tiện ích cụ thể của Express.

Copy
const app = await NestFactory.create<NestExpressApplication>(AppModule);

Bây giờ, chúng ta có thể định nghĩa bộ phân tích truy vấn của ứng dụng, cho phép Nest biết rằng ứng dụng của chúng ta sẽ xử lý các tham số truy vấn phức tạp.

Copy
app.set('query parser', 'extended');

Xác Thực: Một Lập Trình Viên Thực Dụng Luôn Bao Gồm Xác Thực

Chỉ cần như vậy! Giờ đây, ứng dụng Nest của bạn có thể xử lý các truy vấn phức tạp như: dateRange[lte]=2023-10-26T10:00:00.000Z&search=something. Nhưng điều gì sẽ xảy ra nếu ai đó gửi một SQL injection hoặc điều gì đó có thể làm sập ứng dụng của bạn? Là các kỹ sư phần mềm, chúng ta cần đảm bảo rằng chúng ta đang xây dựng các ứng dụng mạnh mẽ.

Để làm được điều này, chúng ta sẽ tuân theo một cấu trúc đơn giản bằng cách tạo các DTO cho biết máy chủ của chúng ta muốn nhận những thuộc tính nào. Hãy bắt đầu với các trường tìm kiếm. Tạo một tệp mới với một lớp gọi là SearchFieldsDto sẽ định nghĩa các thuộc tính hợp lệ mà khách hàng có thể tìm kiếm. Tôi sẽ giữ nó đơn giản bằng cách chỉ khai báo hai thuộc tính.

Copy
export class SearchFieldsDto {
  @IsOptional()
  @IsString()
  @IsAlphanumeric()
  @MinLength(2)
  @MaxLength(30)
  category?: string;

  @IsOptional()
  @IsString()
  @IsAlphanumeric()
  @MinLength(2)
  @MaxLength(20)
  status?: string;
}

Để có cái nhìn chi tiết hơn về các xác thực mà chúng tôi đang sử dụng, tôi khuyên bạn nên kiểm tra tài liệu của class-validator. Bây giờ, hãy xây dựng FiltersDto của chúng ta.

Xây Dựng FiltersDto

Để giữ cho bài viết này ngắn gọn, tôi sẽ thêm một số bộ lọc đơn giản để minh họa cách tiếp cận. Hãy tạo lớp FiltersDto với ba thuộc tính: status, category và dateRange. Chúng ta cũng sẽ tạo một lớp khác cho các toán tử có thể lọc theo khoảng thời gian.

Copy
export class DateRangeDto {
  @IsOptional()
  @IsDateString()
  gte?: Date;

  @IsOptional()
  @IsDateString()
  lte?: Date;
}

export class FiltersDto {
  @IsOptional()
  @IsEnum(['active', 'inactive', 'pending'], {
    message: 'Status must be one of: active, inactive, pending',
  })
  @IsString()
  status?: string;

  @IsOptional()
  @IsString()
  @IsEnum(['electronics', 'furniture', 'clothing'], {
    message: 'Category must be one of: electronics, furniture, clothing',
  })
  category?: string;

  @IsOptional()
  @IsObject()
  dateRange?: DateRangeDto;
}

Với cấu trúc này, chúng ta đang thông báo cho ứng dụng Nest rằng nó có thể xử lý các yêu cầu với các thuộc tính lồng nhau, chẳng hạn như: dateRange[lte]=2023-10-26T10:00:00.000Z.

Lớp QueryDto

Cuối cùng, hãy kết hợp mọi thứ vào một lớp QueryDto duy nhất. Chúng ta sẽ thêm các thuộc tính pagelimit, cùng với một thuộc tính sort chỉ định các trường mà chúng ta có thể sắp xếp và theo hướng nào. Đầu tiên, hãy tạo SortFieldsDto.

Copy
export class SortFieldsDto {
  @IsOptional()
  @IsString()
  @IsIn(['asc', 'desc'])
  category?: 'asc' | 'desc';
}

Bây giờ khi ai đó gửi tham số sort, nó chỉ có thể sắp xếp theo category và chỉ chấp nhận các giá trị "asc" hoặc "desc" - một cái gì đó như thế này: sort[category]=asc. Hãy kết hợp mọi thứ vào lớp QueryDto, sẽ mở rộng FiltersDto.

Copy
export class QueryDto extends FiltersDto {
  @IsOptional()
  @IsObject()
  @ValidateNested()
  @Type(() => SearchFieldsDto)
  search?: SearchFieldsDto;

  @IsOptional()
  @IsInt()
  page?: number;

  @IsOptional()
  @IsInt()
  limit?: number;

  @IsOptional()
  @IsObject()
  @ValidateNested()
  @Type(() => SortFieldsDto)
  sort?: SortFieldsDto;
}

Chúng ta có thể tách mọi thứ thành các DTO riêng biệt và nhóm chúng lại với nhau bằng cách sử dụng tiện ích Intersection, nhưng tôi nghĩ rằng cách tiếp cận này thể hiện tốt hơn cách cấu trúc DTO về mặt hình ảnh. Vậy là bạn đã có nó - API của bạn giờ đây đã được xác thực và ngăn chặn SQL hoặc RSQL injection... hay chưa? Còn chưa.

ValidationPipe Đến Giải Cứu

Nếu bạn gửi một yêu cầu như dateRange[lt]=today&search[magumbos]=something với cấu hình dự án hiện tại, NestJS sẽ chấp nhận nó như một yêu cầu hợp lệ vì chúng ta chưa xác định rõ các quy tắc xác thực và chuyển đổi của mình. Để khắc phục điều này, hãy đi đến tệp main.ts và thêm phương thức useGlobalPipes trước dòng app.listen.

Copy
app.useGlobalPipes();

Phương thức này yêu cầu một thể hiện của ValidationPipe để hoạt động. Thể hiện xác thực chấp nhận một đối tượng chứa các cặp khóa-giá trị xác định các quy tắc xác thực và chuyển đổi cho toàn bộ dự án. Bạn có thể cấu hình các quy tắc này ở các cấp độ khác nhau (theo mô-đun, bộ điều khiển, v.v.), nhưng đó là một chủ đề cho bài viết khác.

Copy
new ValidationPipe({
  whitelist: true,
  transform: true,
  forbidNonWhitelisted: true,
  transformOptions: { enableImplicitConversion: true },
}),

Với cấu hình này, chúng ta đang hướng dẫn ống xác thực rằng tất cả các thuộc tính nên được đưa vào danh sách trắng, được chuyển đổi và bất kỳ thuộc tính nào không có trong danh sách trắng nên bị cấm. Chúng tôi cũng đang cho phép chuyển đổi đối tượng ngầm định. Kết quả là, khi bạn gửi một yêu cầu như dateRange[lt]=today, bạn sẽ nhận được phản hồi yêu cầu không hợp lệ cho biết rằng thuộc tính lt không nên có mặt.

Nếu bạn gửi yêu cầu này: http://localhost:3000/?dateRange[lte]=2023-10-26T10:00:00.000Z&search[status]=so&page=1&limit=10&sort[category]=asc, bạn sẽ nhận được kết quả:

Copy
Hello World! Query: {"dateRange":{"lte":"2023-10-26T10:00:00.000Z"},"search":{"status":"so"},"page":1,"limit":10,"sort":{"category":"asc"}}

Kết Luận

Với ví dụ đơn giản này, tôi đã minh họa tầm quan trọng của việc trở thành một lập trình viên phần mềm thay vì hoàn toàn phụ thuộc vào các công cụ AI. Tôi đã mất vài phút để hỏi ChatGPT, Claude và DeepSeek về cách xử lý loại truy vấn này trong NestJS.

Tất cả đều đề xuất các giải pháp phức tạp và các cách tiếp cận, trong khi tôi chỉ cần đọc tài liệu để nhận ra rằng đó chỉ là một thay đổi dòng lệnh. Bài học ở đây là trong khi việc sử dụng các công cụ mới để giúp giải quyết vấn đề là rất tốt, luôn cần phát triển các giải pháp của riêng bạn.

Hãy tiếp tục học hỏi, và hẹn gặp bạn trong bài viết tiếp theo. Bạn có thể tìm thấy toàn bộ mã cho ví dụ này trong kho GitHub sau: https://github.com/RubenOAlvarado/complex-query-filters.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào