Pipe là một class được đánh dấu bằng decorator @Injectable(), implement interface PipeTransform.
Pipe có hai trường hợp sử dụng điển hình:
- transformation (chuyển đổi): chuyển đổi dữ liệu đầu vào sang dạng mong muốn (ví dụ, từ string sang integer)
- validation (kiểm tra dữ liệu): đánh giá dữ liệu đầu vào và nếu hợp lệ, chỉ đơn giản cho nó đi qua không thay đổi; ngược lại, throw ra một exception
Trong cả hai trường hợp, pipe thao tác trên các arguments đang được xử lý bởi một route handler của controller. Nest chèn một pipe ngay trước khi một method được gọi, và pipe nhận các argument dành cho method đó rồi thao tác trên chúng. Bất kỳ thao tác transformation hay validation nào cũng diễn ra tại thời điểm đó, sau đó route handler được gọi với các argument (có thể) đã được chuyển đổi.
Nest có sẵn một số pipe mà bạn có thể dùng ngay (out-of-the-box). Bạn cũng có thể tự xây dựng custom pipe. Trong chương này, ta sẽ giới thiệu các pipe có sẵn và cách gắn chúng vào route handler. Sau đó ta sẽ xem xét một vài custom pipe để thấy cách bạn có thể tự xây dựng một pipe từ đầu.
Gợi ý Pipe chạy bên trong exceptions zone. Nghĩa là khi một Pipe throw ra một exception, nó sẽ được xử lý bởi exceptions layer (global exceptions filter và bất kỳ exception filter nào được áp dụng cho ngữ cảnh hiện tại). Từ đó, có thể thấy rõ rằng khi một exception được throw trong một Pipe, không có controller method nào sau đó được thực thi. Điều này cho bạn một kỹ thuật best-practice để kiểm tra dữ liệu đi vào ứng dụng từ các nguồn bên ngoài, ngay tại ranh giới của hệ thống.
Ghi chú người dịch Cơ chế này giống việc dùng error-handling middleware tập trung ở cuối chain trong Express, hoặc trả error sớm rồi dừng xử lý như ở Go — khác biệt là bạn không cần tự viết try/catch hay kiểm tra err != nil trong từng handler, vì pipe tự chặn request lại và đẩy exception vào tầng xử lý chung của Nest.
Pipe có sẵn (Built-in pipes)
Nest có sẵn một vài pipe có thể dùng ngay:
ValidationPipeStandardSchemaValidationPipeParseIntPipeParseFloatPipeParseBoolPipeParseArrayPipeParseUUIDPipeParseEnumPipeDefaultValuePipeParseFilePipeParseDatePipe
Chúng được export từ package @nestjs/common.
Hãy xem nhanh cách dùng ParseIntPipe. Đây là một ví dụ về trường hợp sử dụng transformation, trong đó pipe đảm bảo một tham số của method handler được chuyển đổi thành một số nguyên JavaScript (hoặc throw ra một exception nếu việc chuyển đổi thất bại). Sau này trong chương, ta sẽ trình bày một cách triển khai đơn giản cho ParseIntPipe. Các kỹ thuật ví dụ bên dưới cũng áp dụng cho các pipe transformation có sẵn khác (ParseBoolPipe, ParseFloatPipe, ParseEnumPipe, ParseArrayPipe, ParseDatePipe, và ParseUUIDPipe, mà ta sẽ gọi chung là các pipe Parse* trong chương này).
Gắn pipe (Binding pipes)
Để dùng một pipe, ta cần gắn một instance của pipe class vào ngữ cảnh phù hợp. Trong ví dụ ParseIntPipe, ta muốn gắn pipe này với một route handler method cụ thể, và đảm bảo nó chạy trước khi method được gọi. Ta làm điều đó bằng cấu trúc sau, mà ta sẽ gọi là gắn pipe ở cấp độ tham số của method:
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
Điều này đảm bảo một trong hai điều kiện sau là đúng: hoặc tham số ta nhận được trong method findOne() là một số (đúng như mong đợi trong lệnh gọi this.catsService.findOne()), hoặc một exception sẽ được throw ra trước khi route handler được gọi.
Ví dụ, giả sử route được gọi như sau:
GET localhost:3000/abc
Nest sẽ throw ra một exception như sau:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
Exception này sẽ ngăn phần thân của method findOne() được thực thi.
Trong ví dụ trên, ta truyền vào một class (ParseIntPipe), không phải một instance, để framework chịu trách nhiệm khởi tạo và cho phép tiêm phụ thuộc. Tương tự như guard và exception filter, ta cũng có thể truyền vào một instance tại chỗ. Truyền một instance tại chỗ hữu ích nếu ta muốn tùy chỉnh hành vi của pipe có sẵn bằng cách truyền các option:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
Ghi chú người dịch Đây là điểm dễ nhầm khi vibe-code: nếu bạn tự new ParseIntPipe() thay vì để Nest khởi tạo qua class, pipe đó vẫn được inject bình thường vì bản thân nó không phụ thuộc provider nào — nhưng nếu bạn viết custom pipe có constructor cần inject một service khác, truyền instance tại chỗ theo cách này sẽ khiến DI container không có cơ hội inject, và bạn phải tự tay truyền dependency vào, giống cách truyền dependency thủ công qua constructor ở Go.
Việc gắn các pipe transformation khác (tất cả các pipe Parse*) cũng hoạt động tương tự. Các pipe này đều hoạt động trong ngữ cảnh kiểm tra route parameter, query string parameter và giá trị request body.
Ví dụ với một query string parameter:
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
Đây là một ví dụ dùng ParseUUIDPipe để parse một string parameter và kiểm tra xem nó có phải UUID không.
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
@Get(':uuid')
@Bind(Param('uuid', new ParseUUIDPipe()))
async findOne(uuid) {
return this.catsService.findOne(uuid);
}
Gợi ý Khi dùng ParseUUIDPipe(), bạn đang parse UUID phiên bản 3, 4 hoặc 5; nếu bạn chỉ cần một phiên bản UUID cụ thể, bạn có thể truyền vào phiên bản đó trong option của pipe.
Ở trên ta đã thấy các ví dụ gắn các pipe có sẵn thuộc họ Parse* khác nhau. Việc gắn validation pipe hơi khác một chút; ta sẽ bàn về điều đó ở phần tiếp theo.
Gợi ý Ngoài ra, xem Kỹ thuật validation để có thêm nhiều ví dụ chi tiết về validation pipe.
Custom pipe
Như đã đề cập, bạn có thể tự xây dựng custom pipe. Mặc dù Nest cung cấp sẵn ParseIntPipe và ValidationPipe khá mạnh mẽ, hãy cùng xây dựng các phiên bản đơn giản của mỗi loại từ đầu để xem custom pipe được xây dựng như thế nào.
Ta bắt đầu với một ValidationPipe đơn giản. Ban đầu, ta sẽ cho nó nhận một giá trị đầu vào và trả về không thay đổi, hoạt động như một hàm identity.
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class ValidationPipe {
transform(value, metadata) {
return value;
}
}
Gợi ý PipeTransform<T, R> là một generic interface mà mọi pipe đều phải implement. Interface generic này dùng T để biểu thị kiểu của value đầu vào, và R để biểu thị kiểu trả về của method transform().
Mọi pipe đều phải implement method transform() để đáp ứng hợp đồng của interface PipeTransform. Method này có hai tham số:
valuemetadata
Tham số value là argument của method hiện đang được xử lý (trước khi nó được nhận bởi route handling method), và metadata là metadata của argument đó. Metadata object có các thuộc tính sau:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
schema?: StandardSchemaV1;
}
Các thuộc tính này mô tả argument đang được xử lý.
type
|
Cho biết argument là body
@Body(), query
@Query(), param
@Param(), hay một tham số tùy chỉnh (đọc thêm
tại đây). |
metatype
|
Cung cấp metatype của argument, ví dụ,
String. Lưu ý: giá trị này sẽ là
undefined nếu bạn bỏ qua khai báo kiểu trong chữ ký của route handler method, hoặc dùng vanilla JavaScript.
|
data
|
Chuỗi được truyền vào decorator, ví dụ
@Body('string'). Nó là
undefined nếu bạn để trống ngoặc đơn của decorator. |
schema
|
Một schema tương thích Standard Schema được gắn thông qua option của parameter decorator, chẳng hạn như @Body({ schema }) hoặc @Param('id', { schema }).
|
Cảnh báo Interface TypeScript sẽ biến mất trong quá trình transpile. Do đó, nếu kiểu của một method parameter được khai báo là một interface thay vì một class, giá trị metatype sẽ là Object.
Gợi ý StandardSchemaValidationPipe có sẵn sẽ đọc field metadata schema để bạn có thể kiểm tra route parameter bằng bất kỳ thư viện nào tương thích với Standard Schema.
Validation dựa trên schema (Schema based validation)
Hãy làm cho validation pipe của ta hữu ích hơn một chút. Xem xét kỹ hơn method create() của CatsController, nơi ta chắc chắn muốn đảm bảo post body object hợp lệ trước khi thử chạy service method của mình.
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Post()
async create(@Body() createCatDto) {
this.catsService.create(createCatDto);
}
Hãy tập trung vào tham số body createCatDto. Kiểu của nó là CreateCatDto:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
Ta muốn đảm bảo mọi request gửi đến method create đều chứa một body hợp lệ. Vì vậy ta phải kiểm tra ba thành viên của object createCatDto. Ta có thể làm việc này bên trong route handler method, nhưng làm vậy không lý tưởng vì nó sẽ phá vỡ nguyên tắc đơn trách nhiệm (single responsibility principle - SRP).
Một cách tiếp cận khác có thể là tạo một validator class và ủy quyền việc này cho nó. Cách này có nhược điểm là ta phải nhớ gọi validator này ở đầu mỗi method.
Vậy còn tạo validation middleware thì sao? Cách này có thể hoạt động, nhưng tiếc là không thể tạo middleware generic dùng được xuyên suốt mọi ngữ cảnh trong toàn bộ ứng dụng. Lý do là middleware không nhận biết được execution context (ngữ cảnh thực thi), bao gồm cả handler sẽ được gọi lẫn các tham số của nó.
Đây chính xác là trường hợp mà pipe được thiết kế để giải quyết. Vậy hãy tiếp tục và tinh chỉnh validation pipe của ta.
Validation object schema (Object schema validation)
Có nhiều cách tiếp cận khác nhau để validate object theo hướng gọn gàng, DRY. Một cách phổ biến là dùng validation dựa trên schema. Hãy cùng thử cách này.
Thư viện Zod cho phép bạn tạo schema một cách đơn giản, với API dễ đọc. Hãy xây dựng một validation pipe sử dụng schema dựa trên Zod.
Bắt đầu bằng cách cài đặt package cần thiết:
$ npm install --save zod
Trong đoạn code mẫu bên dưới, ta tạo một class đơn giản nhận một schema làm tham số constructor. Sau đó ta áp dụng method schema.parse(), để validate argument đầu vào theo schema đã cung cấp.
Như đã đề cập trước đó, một validation pipe hoặc trả về giá trị không đổi, hoặc throw ra một exception.
Ở phần tiếp theo, bạn sẽ thấy cách ta cung cấp schema phù hợp cho một controller method cụ thể bằng decorator @UsePipes(). Làm như vậy giúp validation pipe của ta có thể tái sử dụng trên nhiều ngữ cảnh, đúng như mục tiêu ban đầu.
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException('Validation failed');
}
}
}
import { BadRequestException } from '@nestjs/common';
export class ZodValidationPipe {
constructor(private schema) {}
transform(value, metadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException('Validation failed');
}
}
}
Gắn validation pipe (Binding validation pipes)
Trước đó, ta đã thấy cách gắn các pipe transformation (như ParseIntPipe và các pipe Parse* còn lại).
Gắn validation pipe cũng rất đơn giản.
Trong trường hợp này, ta muốn gắn pipe ở cấp độ gọi method. Trong ví dụ hiện tại, ta cần làm những việc sau để dùng ZodValidationPipe:
- Tạo một instance của
ZodValidationPipe - Truyền schema Zod phù hợp với ngữ cảnh vào constructor của pipe
- Gắn pipe vào method
Ví dụ về Zod schema:
import { z } from 'zod';
export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();
export type CreateCatDto = z.infer<typeof createCatSchema>;
Ta làm điều đó bằng decorator @UsePipes() như bên dưới:
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Post()
@Bind(Body())
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(createCatDto) {
this.catsService.create(createCatDto);
}
Gợi ý Decorator @UsePipes() được import từ package @nestjs/common.
ZodValidationPipe có thể được áp dụng cho các tham số cụ thể và song song với các pipe có sẵn. Ví dụ, khi ta muốn validate tham số id trên route path bằng ParseIntPipe một cách riêng biệt với request body:
@Put('/:id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body(new ZodValidationPipe(createCatSchema)) body: CreateCatDto
): void {
this.catsService.update(id, body);
}
Cảnh báo Thư viện zod yêu cầu cấu hình strictNullChecks phải được bật trong file tsconfig.json của bạn.
Class validator
Cảnh báo Các kỹ thuật trong phần này yêu cầu TypeScript và không khả dụng nếu ứng dụng của bạn được viết bằng vanilla JavaScript.
Hãy xem một cách triển khai khác cho kỹ thuật validation của ta.
Nest hoạt động tốt với thư viện class-validator. Thư viện mạnh mẽ này cho phép bạn dùng validation dựa trên decorator. Validation dựa trên decorator cực kỳ mạnh mẽ, đặc biệt khi kết hợp với khả năng Pipe của Nest vì ta có quyền truy cập metatype của property đang được xử lý. Trước khi bắt đầu, ta cần cài đặt các package cần thiết:
$ npm i --save class-validator class-transformer
Sau khi cài đặt xong, ta có thể thêm một vài decorator vào class CreateCatDto. Ở đây ta thấy một lợi thế đáng kể của kỹ thuật này: class CreateCatDto vẫn là nguồn chân lý duy nhất (single source of truth) cho post body object của ta (thay vì phải tạo một class validation riêng).
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
Gợi ý Đọc thêm về các decorator của class-validator tại đây.
Giờ ta có thể tạo một class ValidationPipe sử dụng các annotation này.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
Gợi ý Nhắc lại, bạn không cần tự xây dựng một generic validation pipe vì ValidationPipe đã được Nest cung cấp sẵn. ValidationPipe có sẵn cung cấp nhiều option hơn so với ví dụ ta xây dựng trong chương này, vốn được giữ đơn giản chỉ để minh họa cơ chế hoạt động của một custom pipe. Bạn có thể xem đầy đủ chi tiết, cùng nhiều ví dụ tại đây.
Lưu ý Ta đã dùng thư viện class-transformer ở trên, vốn được tạo bởi cùng tác giả với thư viện class-validator, và do đó, chúng phối hợp rất tốt với nhau.
Hãy cùng xem qua đoạn code này. Đầu tiên, lưu ý rằng method transform() được đánh dấu là async. Điều này khả thi vì Nest hỗ trợ cả pipe đồng bộ lẫn pipe bất đồng bộ. Ta làm method này async vì một số validation của class-validator có thể là bất đồng bộ (sử dụng Promise).
Tiếp theo, lưu ý ta dùng destructuring để lấy ra field metatype (chỉ trích xuất riêng thành viên này từ ArgumentMetadata) vào tham số metatype của ta. Đây chỉ là cách viết tắt cho việc lấy toàn bộ ArgumentMetadata rồi thêm một câu lệnh để gán biến metatype.
Tiếp theo, lưu ý hàm hỗ trợ toValidate(). Nó chịu trách nhiệm bỏ qua bước validation khi argument hiện tại đang được xử lý là một kiểu JavaScript nguyên bản (các kiểu này không thể gắn validation decorator, nên không có lý do gì để chạy chúng qua bước validation).
Tiếp theo, ta dùng hàm plainToInstance() của class-transformer để chuyển đổi argument object JavaScript thuần túy của ta thành một object có kiểu, để ta có thể áp dụng validation. Lý do ta phải làm điều này là vì post body object đầu vào, khi được deserialize từ network request, không mang theo thông tin kiểu nào cả (đây là cách platform bên dưới, như Express, hoạt động). Class-validator cần dùng các validation decorator ta đã định nghĩa cho DTO trước đó, nên ta cần thực hiện việc chuyển đổi này để xử lý body đầu vào như một object đã được decorator hóa phù hợp, chứ không chỉ là một object thuần túy thông thường.
Ghi chú người dịch Cạm bẫy hay gặp: class-validator chỉ đọc được các decorator (@IsString(), @IsInt()…) khi giá trị là một instance thật của class đó, không phải object JSON thuần. Nếu bạn định nghĩa DTO bằng interface (quen tay từ code TypeScript phía FE) thì metatype sẽ mất đi khi transpile, toValidate() sẽ luôn bỏ qua, và validation coi như không hoạt động dù bạn tưởng đã gắn đủ decorator.
Cuối cùng, như đã đề cập, vì đây là một validation pipe nên nó hoặc trả về giá trị không đổi, hoặc throw ra một exception.
Bước cuối cùng là gắn ValidationPipe. Pipe có thể có phạm vi tham số, phạm vi method, phạm vi controller, hoặc phạm vi global. Trước đó, với validation pipe dựa trên Zod, ta đã thấy một ví dụ về gắn pipe ở cấp độ method.
Trong ví dụ bên dưới, ta sẽ gắn instance pipe vào decorator @Body() của route handler để pipe của ta được gọi nhằm validate post body.
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
Pipe phạm vi tham số hữu ích khi logic validation chỉ liên quan đến một tham số cụ thể.
Pipe phạm vi global (Global scoped pipes)
Vì ValidationPipe được tạo ra để càng generic càng tốt, ta có thể tận dụng tối đa lợi ích của nó bằng cách thiết lập nó như một pipe phạm vi global, để nó được áp dụng cho mọi route handler trên toàn bộ ứng dụng.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();
Lưu ý Trong trường hợp hybrid app, method useGlobalPipes() không thiết lập pipe cho gateway và microservice. Với ứng dụng microservice “chuẩn” (không phải hybrid), useGlobalPipes() vẫn gắn pipe ở phạm vi global.
Pipe phạm vi global được dùng trong toàn bộ ứng dụng, cho mọi controller và mọi route handler.
Lưu ý rằng về mặt tiêm phụ thuộc, các pipe global được đăng ký từ bên ngoài bất kỳ module nào (dùng useGlobalPipes() như ví dụ trên) không thể inject dependency vì việc gắn được thực hiện bên ngoài ngữ cảnh của bất kỳ module nào. Để giải quyết vấn đề này, bạn có thể thiết lập một pipe global trực tiếp từ bất kỳ module nào bằng cấu trúc sau:
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
Gợi ý Khi dùng cách tiếp cận này để thực hiện tiêm phụ thuộc cho pipe, lưu ý rằng bất kể module nào được dùng để khai báo cấu trúc này, pipe thực chất vẫn là global. Vậy nên đặt ở đâu? Hãy chọn module nơi pipe (ValidationPipe trong ví dụ trên) được định nghĩa. Ngoài ra, useClass không phải là cách duy nhất để xử lý việc đăng ký custom provider. Tìm hiểu thêm tại đây.
ValidationPipe có sẵn (The built-in ValidationPipe)
Nhắc lại, bạn không cần tự xây dựng một generic validation pipe vì ValidationPipe đã được Nest cung cấp sẵn. ValidationPipe có sẵn cung cấp nhiều option hơn so với ví dụ ta xây dựng trong chương này, vốn được giữ đơn giản chỉ để minh họa cơ chế hoạt động của một custom pipe. Bạn có thể xem đầy đủ chi tiết, cùng nhiều ví dụ tại đây.
Trường hợp sử dụng transformation (Transformation use case)
Validation không phải trường hợp sử dụng duy nhất cho custom pipe. Ở đầu chương này, ta đã đề cập rằng một pipe cũng có thể chuyển đổi (transform) dữ liệu đầu vào sang định dạng mong muốn. Điều này khả thi vì giá trị trả về từ hàm transform sẽ hoàn toàn ghi đè lên giá trị trước đó của argument.
Khi nào điều này hữu ích? Hãy xem xét trường hợp đôi khi dữ liệu được truyền từ client cần trải qua một số thay đổi - ví dụ chuyển đổi một string sang integer - trước khi nó có thể được xử lý đúng cách bởi route handler method. Hơn nữa, một số trường dữ liệu bắt buộc có thể bị thiếu, và ta muốn áp dụng giá trị mặc định. Các transformation pipe có thể thực hiện những chức năng này bằng cách chèn một hàm xử lý vào giữa client request và request handler.
Đây là một ParseIntPipe đơn giản, chịu trách nhiệm parse một string thành một giá trị integer. (Như đã đề cập ở trên, Nest có sẵn một ParseIntPipe tinh vi hơn; ta đưa cái này vào như một ví dụ đơn giản về custom transformation pipe).
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
import { Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe {
transform(value, metadata) {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
Sau đó ta có thể gắn pipe này vào tham số đã chọn như bên dưới:
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
@Get(':id')
@Bind(Param('id', new ParseIntPipe()))
async findOne(id) {
return this.catsService.findOne(id);
}
Một trường hợp transformation hữu ích khác là chọn ra một user entity đã tồn tại từ database bằng id được cung cấp trong request:
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
@Get(':id')
@Bind(Param('id', UserByIdPipe))
findOne(userEntity) {
return userEntity;
}
Ta để việc triển khai pipe này cho bạn đọc, nhưng lưu ý rằng giống như mọi transformation pipe khác, nó nhận một giá trị đầu vào (một id) và trả về một giá trị đầu ra (một object UserEntity). Điều này có thể giúp code của bạn khai báo rõ ràng hơn và DRY hơn bằng cách tách code khung (boilerplate) ra khỏi handler của bạn và đưa vào một pipe dùng chung.
Cung cấp giá trị mặc định (Providing defaults)
Các pipe Parse* yêu cầu giá trị của một tham số phải được xác định (defined). Chúng sẽ throw ra một exception khi nhận giá trị null hoặc undefined. Để cho phép một endpoint xử lý các giá trị tham số querystring bị thiếu, ta phải cung cấp một giá trị mặc định được inject trước khi các pipe Parse* thao tác trên các giá trị này. DefaultValuePipe phục vụ mục đích đó. Chỉ cần khởi tạo một DefaultValuePipe trong decorator @Query() trước pipe Parse* tương ứng, như bên dưới:
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}