Controller chịu trách nhiệm xử lý các request đến và gửi response trả về cho client.

Mục đích của một controller là xử lý các request cụ thể cho ứng dụng. Cơ chế routing sẽ quyết định controller nào xử lý request nào. Thông thường, một controller có nhiều route, và mỗi route có thể thực hiện một hành động khác nhau.
Để tạo một controller cơ bản, chúng ta dùng class và decorator. Decorator gắn kết class với metadata cần thiết, cho phép Nest tạo ra một routing map kết nối các request với controller tương ứng.
Gợi ý Để nhanh chóng tạo một CRUD controller có sẵn validation, bạn có thể dùng CRUD generator của CLI: nest g resource [name].
Routing
Trong ví dụ dưới đây, chúng ta sẽ dùng decorator @Controller(), decorator này là bắt buộc để định nghĩa một controller cơ bản. Chúng ta sẽ chỉ định một route path prefix tùy chọn là cats. Việc dùng path prefix trong decorator @Controller() giúp chúng ta nhóm các route liên quan lại với nhau và giảm việc lặp code. Ví dụ, nếu chúng ta muốn nhóm các route quản lý tương tác với entity cat dưới path /cats, chúng ta có thể chỉ định prefix cats trong decorator @Controller(). Nhờ vậy, chúng ta không cần lặp lại phần path đó cho mỗi route trong file.
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
return 'This action returns all cats';
}
}
Gợi ý Để tạo một controller bằng CLI, chỉ cần chạy lệnh $ nest g controller [name].
Decorator @Get() cho HTTP request method, đặt trước method findAll(), báo cho Nest tạo một hàm xử lý (handler) cho một endpoint cụ thể ứng với các HTTP request. Endpoint này được xác định bởi HTTP request method (trong trường hợp này là GET) và route path. Vậy route path là gì? Route path của một handler được xác định bằng cách kết hợp prefix (tùy chọn) đã khai báo cho controller với path (nếu có) được chỉ định trong decorator của method. Vì chúng ta đã đặt prefix (cats) cho mọi route và không thêm path cụ thể nào trong decorator của method, Nest sẽ map các request GET /cats tới handler này.
Như đã đề cập, route path bao gồm cả prefix tùy chọn của controller và bất kỳ chuỗi path nào được chỉ định trong decorator của method. Ví dụ, nếu prefix của controller là cats và decorator của method là @Get('breed'), route kết quả sẽ là GET /cats/breed.
Trong ví dụ trên, khi một GET request được gửi tới endpoint này, Nest sẽ route request đó tới method findAll() do người dùng định nghĩa. Lưu ý rằng tên method chúng ta chọn ở đây hoàn toàn tùy ý. Dù bắt buộc phải khai báo một method để gắn (bind) route vào, Nest không gán bất kỳ ý nghĩa đặc biệt nào cho tên method đó.
Method này sẽ trả về status code 200 kèm response tương ứng, trong trường hợp này chỉ là một chuỗi (string). Tại sao lại như vậy? Để giải thích, trước tiên chúng ta cần giới thiệu khái niệm: Nest dùng hai cách tiếp cận khác nhau để thao tác với response:
| Standard (khuyến nghị) |
Với method có sẵn này, khi một hàm xử lý request trả về một JavaScript object hoặc array, nó sẽ được tự động
tuần tự hóa (serialize) thành JSON. Tuy nhiên khi trả về một kiểu nguyên thủy (primitive type) của JavaScript (ví dụ: string, number, boolean), Nest sẽ chỉ gửi giá trị đó mà không cố serialize nó. Điều này giúp việc xử lý response trở nên đơn giản: chỉ cần trả về giá trị, và Nest lo phần còn lại.
Hơn nữa, status code của response mặc định luôn là 200, ngoại trừ các request POST dùng status code 201. Chúng ta có thể dễ dàng thay đổi hành vi này bằng cách thêm decorator @HttpCode(...)
ở cấp handler (xem Status codes).
|
| Library-specific |
Chúng ta có thể dùng response object đặc thù theo thư viện (ví dụ: Express), object này có thể được inject bằng decorator @Res() trong signature của method handler (ví dụ: findAll(@Res() response)). Với cách này, bạn có khả năng dùng các method xử lý response gốc mà object đó cung cấp. Ví dụ, với Express, bạn có thể xây dựng response bằng đoạn code như response.status(200).send().
|
Cảnh báo Nest phát hiện khi handler dùng @Res() hoặc @Next(), cho thấy bạn đã chọn cách tiếp cận library-specific. Nếu cả hai cách tiếp cận được dùng cùng lúc, cách tiếp cận Standard sẽ tự động bị vô hiệu hóa cho riêng route đó và sẽ không còn hoạt động như mong đợi. Để dùng cả hai cách tiếp cận cùng lúc (ví dụ, chỉ inject response object để set cookie/header còn lại vẫn để framework xử lý), bạn phải set option passthrough thành true trong decorator @Res({ passthrough: true }).
Ghi chú người dịch Cạm bẫy thực tế hay gặp: khi đã inject @Res() mà không set passthrough: true, nếu bạn quên gọi res.send()/res.json()/res.end() (ví dụ do return sớm hoặc xử lý lỗi không đúng cách), request sẽ treo vô thời hạn vì Nest không còn tự gửi response giúp bạn nữa - khác hẳn cách tiếp cận Standard, chỉ cần return giá trị là xong.
Request object
Handler thường cần truy cập vào thông tin chi tiết của request từ client. Nest cung cấp quyền truy cập vào request object từ nền tảng bên dưới (Express theo mặc định). Bạn có thể truy cập request object bằng cách yêu cầu Nest inject nó thông qua decorator @Req() trong signature của handler.
import { Controller, Get, Req } from '@nestjs/common';
import type { Request } from 'express';
@Controller('cats')
export class CatsController {
@Get()
findAll(@Req() request: Request): string {
return 'This action returns all cats';
}
}
import { Controller, Bind, Get, Req } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
@Bind(Req())
findAll(request) {
return 'This action returns all cats';
}
}
Gợi ý Để tận dụng typings của express (như trong ví dụ tham số request: Request ở trên), hãy đảm bảo cài đặt package @types/express.
Request object đại diện cho HTTP request và chứa các property cho query string, tham số (parameters), HTTP header và body (đọc thêm tại đây). Trong hầu hết trường hợp, bạn không cần truy cập trực tiếp các property này. Thay vào đó, bạn có thể dùng các decorator riêng như @Body() hoặc @Query(), vốn đã có sẵn. Dưới đây là danh sách các decorator được cung cấp và object đặc thù theo nền tảng tương ứng mà chúng đại diện.
@Request(), @Req() |
req |
@Response(), @Res()* |
res |
@Next() |
next |
@Session() |
req.session |
@Param(key?: string) |
req.params / req.params[key] |
@Body(key?: string) |
req.body / req.body[key] |
@Query(key?: string) |
req.query / req.query[key] |
@Headers(name?: string) |
req.headers / req.headers[name] |
@Ip() |
req.ip |
@HostParam() |
req.hosts |
* Để tương thích typings trên các nền tảng HTTP bên dưới (ví dụ: Express và Fastify), Nest cung cấp decorator @Res() và @Response(). @Res() chỉ đơn giản là alias của @Response(). Cả hai đều expose trực tiếp interface của native response object của nền tảng bên dưới. Khi dùng chúng, bạn cũng nên import typings cho thư viện bên dưới (ví dụ: @types/express) để tận dụng tối đa. Lưu ý rằng khi bạn inject @Res() hoặc @Response() trong một method handler, bạn đưa Nest vào chế độ Library-specific cho handler đó, và bạn trở thành người chịu trách nhiệm quản lý response. Khi đó, bạn phải chủ động gửi response bằng cách gọi trên response object (ví dụ: res.json(...) hoặc res.send(...)), nếu không HTTP server sẽ bị treo (hang).
@Body(), @Query(), @Param(), và @RawBody() cũng có thể nhận một options object với schema và pipes. Điều này giúp bạn có thể gắn trực tiếp các schema tương thích Standard Schema vào tham số route, bao gồm các schema được tạo bởi các package như Zod, Valibot và ArkType.
@Post()
create(@Body({ schema: createCatSchema }) createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}
@Get(':id')
findOne(@Param('id', { schema: z.coerce.number().int().positive() }) id: number) {
return this.catsService.findOne(id);
}
Để thực sự validate các schema này, hãy đăng ký StandardSchemaValidationPipe có sẵn hoặc dùng pipe của riêng bạn đọc metadata.schema.
Gợi ý Để học cách tạo custom decorator của riêng bạn, hãy xem chương này.
Resources (Tài nguyên)
Trước đó, chúng ta đã định nghĩa một endpoint để lấy resource cats (route GET). Chúng ta thường cũng muốn cung cấp một endpoint để tạo bản ghi mới. Để làm điều này, hãy tạo handler POST:
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create(): string {
return 'This action adds a new cat';
}
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create() {
return 'This action adds a new cat';
}
@Get()
findAll() {
return 'This action returns all cats';
}
}
Nest cung cấp decorator cho tất cả các HTTP method chuẩn: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head(), và @QueryMethod() (map tới method QUERY và được đặt tên như vậy để tránh xung đột với decorator tham số @Query()). Ngoài ra, @All() định nghĩa một endpoint xử lý tất cả các method trên.
Route wildcard
NestJS cũng hỗ trợ các route dựa trên pattern. Ví dụ, dấu sao (*) có thể được dùng như một wildcard để khớp với bất kỳ tổ hợp ký tự nào ở cuối một route path. Trong ví dụ dưới đây, method findAll() sẽ được thực thi cho bất kỳ route nào bắt đầu bằng abcd/, bất kể có bao nhiêu ký tự theo sau.
@Get('abcd/*')
findAll() {
return 'This route uses a wildcard';
}
Route path 'abcd/*' sẽ khớp với abcd/, abcd/123, abcd/abc, v.v. Dấu gạch ngang (-) và dấu chấm (.) được hiểu theo nghĩa đen (literal) trong các path dạng chuỗi.
Cách tiếp cận này hoạt động trên cả Express lẫn Fastify. Tuy nhiên, Express v5 đã làm cho hệ thống routing trở nên nghiêm ngặt hơn. Trong Express thuần, bạn phải dùng wildcard có tên để route hoạt động - ví dụ, abcd/*splat, trong đó splat chỉ đơn giản là tên của tham số wildcard và không mang ý nghĩa đặc biệt nào. Bạn có thể đặt tên nó tùy ý. Tuy vậy, vì Nest cung cấp một lớp tương thích cho Express, bạn vẫn có thể dùng dấu sao (*) làm wildcard.
Đối với dấu sao được dùng ở giữa route, Express yêu cầu wildcard có tên (ví dụ: ab{*splat}cd), trong khi Fastify hoàn toàn không hỗ trợ trường hợp này.
Xung đột route và thứ tự phân giải
Nest đăng ký các route theo thứ tự khai báo. Trên các adapter nhạy cảm với thứ tự (order-sensitive) - adapter Express mặc định là một ví dụ - điều này có nghĩa là một route dạng tham số (parametric route) có thể âm thầm che khuất (shadow) một route cụ thể hơn:
@Controller('users')
export class UsersController {
@Get(':id')
findOne() {}
@Get('me') // không bao giờ được gọi tới: `:id` khớp với "me" trước
findMe() {}
}
Điều này rất dễ bị bỏ sót, vì ứng dụng vẫn khởi động bình thường mà không có cảnh báo nào, và vấn đề chỉ lộ ra ở runtime, khi một request bị dispatch tới sai handler. Các pipe như ParseIntPipe cũng không giúp được gì ở đây - việc routing chọn handler trước khi bất kỳ pipe nào chạy.
NestJS v12 bổ sung hai option opt-in trên NestApplicationOptions để bảo vệ trước vấn đề này. Cả hai đều mặc định giữ hành vi cũ, nên các ứng dụng hiện có không bị ảnh hưởng trừ khi bạn chủ động set chúng.
routeConflictPolicy kích hoạt chẩn đoán (diagnostics) tại thời điểm bootstrap. Nó nhận một mức độ nghiêm trọng (severity) riêng cho từng loại: 'off', 'warn', hoặc 'error':
const app = await NestFactory.create(AppModule, {
routeConflictPolicy: { duplicate: 'error', shadow: 'warn' },
});
duplicate |
Hai route có cùng method, path, host và version giống hệt nhau. |
shadow |
Hai pattern route có thể cùng khớp với một request, ví dụ /users/me và /users/:id. |
Với 'error', mọi cặp route vi phạm sẽ được gộp lại thành một RouteConflictException duy nhất, được ném ra trong quá trình app.listen(), nhờ vậy bạn thấy hết tất cả các lỗi cùng lúc thay vì chỉ một lỗi mỗi lần restart.
routeResolutionStrategy kiểm soát thứ tự đăng ký. Set nó thành 'specificity' sẽ đăng ký các route cụ thể nhất trước - segment dạng literal thắng segment dạng tham số, segment dạng tham số thắng wildcard - nhờ vậy ví dụ ở trên hoạt động đúng bất kể thứ tự khai báo:
const app = await NestFactory.create(AppModule, {
routeResolutionStrategy: 'specificity',
});
Giá trị mặc định là 'declaration', giữ nguyên hành vi cũ.
Ghi chú người dịch Nếu bạn từng dùng router của Express hay Gin ở Go, chắc bạn còn nhớ nguyên tắc tương tự: route càng cụ thể (literal) nên khai báo trước route càng tổng quát (tham số/wildcard). Nest giữ hành vi “khai báo trước thắng” theo mặc định, nên khi thấy route bị dispatch sai handler dù không có lỗi cú pháp hay warning nào, hãy kiểm tra thứ tự khai báo trước khi nghi ngờ pipe hay guard.
Gợi ý Các option này chỉ có ý nghĩa trên các adapter mà thứ tự đăng ký ảnh hưởng đến việc khớp route. ExpressAdapter nhạy cảm với thứ tự; FastifyAdapter thì không, vì find-my-way đã tự xếp hạng route theo độ cụ thể (specificity). Trên Fastify, policy shadow không có tác dụng gì và việc sắp xếp 'specificity' cũng không ảnh hưởng gì, trong khi policy duplicate vẫn được tôn trọng trên cả hai adapter. Các type RouteConflictPolicy, RouteConflictPolicyLevel, và RouteResolutionStrategy được export từ @nestjs/common.
Status code
Như đã đề cập, status code mặc định cho response luôn là 200, ngoại trừ các request POST, mặc định là 201. Bạn có thể dễ dàng thay đổi hành vi này bằng cách dùng decorator @HttpCode(...) ở cấp handler.
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
Gợi ý Import HttpCode từ package @nestjs/common.
Thông thường, status code của bạn không cố định mà phụ thuộc vào nhiều yếu tố khác nhau. Trong trường hợp đó, bạn có thể dùng object response đặc thù theo thư viện (inject bằng @Res()) (hoặc, trong trường hợp có lỗi, ném ra một exception).
Response header
Để chỉ định một header tùy chỉnh cho response, bạn có thể dùng decorator @Header() hoặc object response đặc thù theo thư viện (và gọi trực tiếp res.header()).
@Post()
@Header('Cache-Control', 'no-store')
create() {
return 'This action adds a new cat';
}
Gợi ý Import Header từ package @nestjs/common.
Redirection (Chuyển hướng)
Để chuyển hướng response tới một URL cụ thể, bạn có thể dùng decorator @Redirect() hoặc object response đặc thù theo thư viện (và gọi trực tiếp res.redirect()).
@Redirect() nhận hai tham số, url và statusCode, cả hai đều là tùy chọn. Giá trị mặc định của statusCode là 302 (Found) nếu bị bỏ qua.
@Get()
@Redirect('https://nestjs.com', 301)
Gợi ý Đôi khi bạn có thể muốn xác định HTTP status code hoặc redirect URL một cách động. Hãy làm điều này bằng cách trả về một object tuân theo interface HttpRedirectResponse (từ @nestjs/common).
Giá trị trả về sẽ ghi đè lên bất kỳ tham số nào được truyền cho decorator @Redirect(). Ví dụ:
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
}
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
@Bind(Query('version'))
getDocs(version) {
if (version && version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
}
Tham số route (Route parameters)
Các route với path tĩnh sẽ không hoạt động khi bạn cần nhận dữ liệu động như một phần của request (ví dụ: GET /cats/1 để lấy con cat có id 1). Để định nghĩa các route có tham số, bạn có thể thêm token tham số route trong route path để bắt các giá trị động từ URL. Ví dụ về token tham số route trong decorator @Get() dưới đây minh họa cách tiếp cận này. Các tham số route này sau đó có thể được truy cập bằng decorator @Param(), decorator này cần được thêm vào signature của method.
Gợi ý Các route có tham số nên được khai báo sau các path tĩnh, để path dạng tham số không “chặn” traffic vốn dành cho path tĩnh. Xem Xung đột route và thứ tự phân giải để biết các option giúp phát hiện vấn đề này tại thời điểm bootstrap, hoặc tự động xử lý giúp bạn.
@Get(':id')
findOne(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
@Get(':id')
@Bind(Param())
findOne(params) {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
Decorator @Param() dùng để decorate một tham số của method (trong ví dụ trên là params), giúp các tham số route có thể truy cập được dưới dạng property của tham số method đã được decorate đó bên trong method. Như trong đoạn code trên, bạn có thể truy cập tham số id bằng cách tham chiếu params.id. Ngoài ra, bạn cũng có thể truyền một token tham số cụ thể vào decorator và tham chiếu trực tiếp tham số route đó theo tên trong nội dung method.
Gợi ý Import Param từ package @nestjs/common.
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}
@Get(':id')
@Bind(Param('id'))
findOne(id) {
return `This action returns a #${id} cat`;
}
Sub-domain routing
Decorator @Controller có thể nhận option host để yêu cầu HTTP host của các request đến phải khớp với một giá trị cụ thể.
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index() {
return 'Admin page';
}
}
Cảnh báo Vì Fastify không hỗ trợ nested router, nếu bạn dùng sub-domain routing, bạn nên dùng adapter Express mặc định thay vì Fastify.
Tương tự như route path, option host cũng có thể dùng token để bắt giá trị động ở vị trí đó trong tên host. Ví dụ về token tham số host trong decorator @Controller() dưới đây minh họa cách dùng này. Các tham số host được khai báo theo cách này có thể được truy cập bằng decorator @HostParam(), decorator này cần được thêm vào signature của method.
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
@Bind(HostParam('account'))
getInfo(account) {
return account;
}
}
Chia sẻ state
Đối với các developer đến từ các ngôn ngữ lập trình khác, có thể sẽ bất ngờ khi biết rằng trong Nest, gần như mọi thứ đều được chia sẻ (shared) giữa các request đến. Điều này bao gồm các tài nguyên như connection pool của database, các service singleton có global state, và nhiều hơn nữa. Điều quan trọng cần hiểu là Node.js không dùng Multi-Threaded Stateless Model cho request/response, nơi mỗi request được xử lý bởi một thread riêng biệt. Do đó, việc dùng instance singleton trong Nest hoàn toàn an toàn cho ứng dụng của chúng ta.
Ghi chú người dịch Đây là khác biệt lớn so với PHP truyền thống (Apache/PHP-FPM), nơi mỗi request chạy trên một process/interpreter riêng nên biến toàn cục tự reset. Trong Node/Nest, provider mặc định là singleton sống suốt vòng đời ứng dụng, nên nếu bạn lưu dữ liệu của một request vào property của class (this.something = ...) thay vì biến local, dữ liệu đó có thể bị request khác đọc/ghi đè - đây là lỗi runtime hay gặp nhất khi vibe-code Nest mà chưa hiểu vòng đời singleton.
Tuy nhiên, có một số trường hợp đặc biệt (edge case) mà việc có vòng đời (lifetime) theo request cho controller là cần thiết. Ví dụ như caching theo từng request trong ứng dụng GraphQL, request tracking, hoặc triển khai multi-tenancy. Bạn có thể tìm hiểu thêm về cách kiểm soát phạm vi inject tại đây.
Bất đồng bộ (Asynchronicity)
Chúng tôi yêu thích JavaScript hiện đại, đặc biệt là sự chú trọng vào việc xử lý dữ liệu bất đồng bộ (asynchronous). Đó là lý do Nest hỗ trợ đầy đủ các hàm async. Mỗi hàm async phải trả về một Promise, cho phép bạn trả về một giá trị trì hoãn (deferred value) mà Nest có thể tự động resolve. Đây là một ví dụ:
@Get()
async findAll(): Promise<any[]> {
return [];
}
@Get()
async findAll() {
return [];
}
Đoạn code này hoàn toàn hợp lệ. Nhưng Nest còn tiến xa hơn khi cho phép các route handler trả về RxJS observable stream. Nest sẽ tự xử lý việc subscribe và resolve giá trị được emit cuối cùng khi stream hoàn tất.
@Get()
findAll(): Observable<any[]> {
return of([]);
}
@Get()
findAll() {
return of([]);
}
Cả hai cách tiếp cận đều hợp lệ, và bạn có thể chọn cách phù hợp nhất với nhu cầu của mình.
Request payload
Trong ví dụ trước, route handler POST của chúng ta chưa nhận bất kỳ tham số nào từ client. Hãy sửa điều đó bằng cách thêm decorator @Body().
Trước khi tiếp tục (nếu bạn dùng TypeScript), chúng ta cần định nghĩa schema DTO (Data Transfer Object). DTO là một object quy định cách dữ liệu sẽ được gửi qua network. Chúng ta có thể định nghĩa schema DTO bằng interface TypeScript hoặc class đơn giản. Tuy nhiên, chúng tôi khuyến nghị dùng class. Tại sao? Class là một phần của chuẩn JavaScript ES6, nên chúng vẫn tồn tại nguyên vẹn như các entity thực sự trong JavaScript đã biên dịch. Ngược lại, interface TypeScript bị loại bỏ trong quá trình transpile, nghĩa là Nest không thể tham chiếu chúng ở runtime. Điều này quan trọng vì các tính năng như Pipe phụ thuộc vào việc có quyền truy cập metatype của biến ở runtime, điều này chỉ khả thi với class.
Ghi chú người dịch Lý do tương tự việc Go không thể reflect ra field của một interface tại runtime - interface chỉ là hợp đồng (contract) chứ không mang theo thông tin type như struct. Class TypeScript đóng vai trò giống struct ở Go hay class ở PHP: nó thực sự tồn tại ở runtime, nên Nest (thông qua reflect-metadata) mới đọc được để Pipe validate hay transform dữ liệu.
Hãy tạo class CreateCatDto:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
Nó chỉ có ba property cơ bản. Bây giờ chúng ta có thể dùng DTO vừa tạo bên trong CatsController:
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
@Post()
@Bind(Body())
async create(createCatDto) {
return 'This action adds a new cat';
}
Gợi ý ValidationPipe của chúng ta có thể lọc bỏ các property mà method handler không nên nhận. Trong trường hợp này, chúng ta có thể whitelist các property được chấp nhận, và mọi property không nằm trong whitelist sẽ tự động bị loại bỏ khỏi object kết quả. Trong ví dụ CreateCatDto, whitelist của chúng ta là các property name, age, và breed. Tìm hiểu thêm tại đây.
Query parameter
Khi xử lý query parameter trong route, bạn có thể dùng decorator @Query() để trích xuất chúng từ request đến. Hãy xem cách này hoạt động trong thực tế.
Xét một route mà chúng ta muốn lọc danh sách cat dựa trên các query parameter như age và breed. Trước tiên, hãy định nghĩa các query parameter trong CatsController:
@Get()
async findAll(@Query('age') age: number, @Query('breed') breed: string) {
return `This action returns all cats filtered by age: ${age} and breed: ${breed}`;
}
@Get()
@Bind(Query('age'), Query('breed'))
async findAll(age, breed) {
return `This action returns all cats filtered by age: ${age} and breed: ${breed}`;
}
Trong ví dụ này, decorator @Query() được dùng để trích xuất giá trị của age và breed từ query string. Ví dụ, một request tới:
GET /cats?age=2&breed=Persian
sẽ cho kết quả age là 2 và breed là Persian.
Nếu ứng dụng của bạn cần xử lý các query parameter phức tạp hơn, chẳng hạn như object lồng nhau hoặc array:
?filter[where][name]=John&filter[where][age]=30
?item[]=1&item[]=2
bạn sẽ cần cấu hình HTTP adapter (Express hoặc Fastify) để dùng một query parser phù hợp. Trong Express, bạn có thể dùng parser extended, cho phép tạo query object phong phú:
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('query parser', 'extended');
const app = await NestFactory.create(AppModule);
app.set('query parser', 'extended');
Trong Fastify, bạn có thể dùng option querystringParser:
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
querystringParser: (str) => qs.parse(str),
}),
);
const app = await NestFactory.create(
AppModule,
new FastifyAdapter({
querystringParser: (str) => qs.parse(str),
}),
);
Gợi ý qs là một query string parser hỗ trợ lồng nhau (nesting) và array. Bạn có thể cài đặt nó bằng npm install qs.
Xử lý lỗi
Có một chương riêng về xử lý lỗi (tức là làm việc với exception) tại đây.
Quan sát route trong môi trường production
Một controller hoạt động hoàn hảo trên máy của bạn có thể hoạt động rất khác dưới lưu lượng traffic thực tế. Câu hỏi thường gặp trong môi trường production không bao giờ là “route này có hoạt động không?” mà là “tại sao GET /cats/:id lại tăng từ 40 ms lên 900 ms sau lần deploy hôm thứ Ba, và đó là mọi request hay chỉ một tenant xui xẻo nào đó?”
Route handler là đơn vị tự nhiên để trả lời câu hỏi đó, và NestJS Observe báo cáo chính xác trên đơn vị này. Vì SDK @nestjs/observe móc (hook) trực tiếp vào vòng đời request của chính Nest thay vì bọc (wrap) HTTP server, mỗi phép đo đều được gắn nhãn theo route pattern bạn đã khai báo - GET /cats/:id, chứ không phải 10.000 URL riêng lẻ - nhờ vậy một route trở thành một dòng duy nhất mà bạn có thể sắp xếp, vẽ biểu đồ và đặt cảnh báo:
const app = await NestFactory.create(AppModule, {
instrument: ObserveInstrument,
});
Đó là toàn bộ việc tích hợp. Từ đó, tìm một route chậm chỉ mất ba cú click: sắp xếp danh sách route theo p95, mở operation để xem liệu sự suy giảm hiệu năng là ổn định hay đột biến và liệu nó có bắt đầu từ một lần release hay không, sau đó mở một lần thực thi chậm và đọc waterfall của nó - controller nào, service method nào, query nào chiếm thời gian. Thời gian được gán theo từng class và method, với mọi thời gian await đã được trừ ra, nên việc CatsService.findOne() tự tốn 800 ms là có thể phân biệt ngay lập tức với việc CatsService.findOne() chờ 800 ms trên database.
Xem chương Observability để thiết lập, và Dashboard để xem toàn bộ hành trình từ một cảnh báo (alert) xuống tới một request cụ thể.
Ví dụ resource đầy đủ
Dưới đây là một ví dụ minh họa việc sử dụng một số decorator có sẵn để tạo một controller cơ bản. Controller này cung cấp vài method để truy cập và thao tác dữ liệu nội bộ.
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto.js';
@Controller('cats')
export class CatsController {
@Post()
create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
@Get()
findAll(@Query() query: ListAllEntities) {
return `This action returns all cats (limit: ${query.limit} items)`;
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns a #${id} cat`;
}
@Put(':id')
update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
return `This action updates a #${id} cat`;
}
@Delete(':id')
remove(@Param('id') id: string) {
return `This action removes a #${id} cat`;
}
}
import { Controller, Get, Query, Post, Body, Put, Param, Delete, Bind } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
@Bind(Body())
create(createCatDto) {
return 'This action adds a new cat';
}
@Get()
@Bind(Query())
findAll(query) {
console.log(query);
return `This action returns all cats (limit: ${query.limit} items)`;
}
@Get(':id')
@Bind(Param('id'))
findOne(id) {
return `This action returns a #${id} cat`;
}
@Put(':id')
@Bind(Param('id'), Body())
update(id, updateCatDto) {
return `This action updates a #${id} cat`;
}
@Delete(':id')
@Bind(Param('id'))
remove(id) {
return `This action removes a #${id} cat`;
}
}
Gợi ý Nest CLI cung cấp một generator (schematic) tự động tạo toàn bộ code boilerplate, giúp bạn không phải làm việc này thủ công và cải thiện trải nghiệm phát triển tổng thể. Tìm hiểu thêm về tính năng này tại đây.
Đưa vào hoạt động
Ngay cả khi CatsController đã được định nghĩa đầy đủ, Nest vẫn chưa biết về nó và sẽ không tự động tạo instance cho class này.
Controller luôn phải là một phần của module, đó là lý do chúng ta thêm array controllers bên trong decorator @Module(). Vì chúng ta chưa định nghĩa module nào khác ngoài AppModule gốc, chúng ta sẽ dùng nó để đăng ký CatsController:
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller.js';
@Module({
controllers: [CatsController],
})
export class AppModule {}
Chúng ta đã gắn metadata vào class module bằng decorator @Module(), và giờ đây Nest có thể dễ dàng xác định những controller nào cần được mount.
Cách tiếp cận đặc thù theo thư viện (Library-specific)
Cho đến giờ, chúng ta đã đề cập đến cách tiếp cận chuẩn của Nest để thao tác với response. Một cách tiếp cận khác là dùng response object đặc thù theo thư viện. Để inject một response object cụ thể, chúng ta có thể dùng decorator @Res(). Để làm rõ sự khác biệt, hãy viết lại CatsController như sau:
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Controller('cats')
export class CatsController {
@Post()
create(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.OK).json([]);
}
}
import { Controller, Get, Post, Bind, Res, Body, HttpStatus } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
@Bind(Res(), Body())
create(res, createCatDto) {
res.status(HttpStatus.CREATED).send();
}
@Get()
@Bind(Res())
findAll(res) {
res.status(HttpStatus.OK).json([]);
}
}
Dù cách tiếp cận này hoạt động và mang lại sự linh hoạt hơn nhờ toàn quyền kiểm soát response object (như thao tác header và truy cập các tính năng đặc thù theo thư viện), bạn nên thận trọng khi dùng nó. Nhìn chung, cách này kém rõ ràng hơn và đi kèm một số nhược điểm. Nhược điểm chính là code của bạn trở nên phụ thuộc vào nền tảng (platform-dependent), vì các thư viện bên dưới khác nhau có thể có API khác nhau cho response object. Ngoài ra, nó cũng khiến việc test trở nên khó khăn hơn, vì bạn sẽ cần mock response object, cùng nhiều thứ khác.
Hơn nữa, khi dùng cách tiếp cận này, bạn sẽ mất khả năng tương thích với các tính năng của Nest dựa vào cách xử lý response chuẩn, chẳng hạn như Interceptor và các decorator @HttpCode() / @Header(). Để khắc phục điều này, bạn có thể bật option passthrough như sau:
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.status(HttpStatus.OK);
return [];
}
@Get()
@Bind(Res({ passthrough: true }))
findAll(res) {
res.status(HttpStatus.OK);
return [];
}
Với cách tiếp cận này, bạn có thể tương tác với native response object (ví dụ: set cookie hoặc header dựa theo điều kiện cụ thể), trong khi vẫn để framework xử lý phần còn lại.