NestJS - Controllers
Trách nhiệm chính của controllers là xử lý các requests và phản hồi lại cho phía client.
Thiết kế kiến trúc Test - Driven Development (TDD) cho dự án với NestJS
NestJS sử dụng một design pattern ở hầu hết mọi nơi, đó là Decorator (nếu ai từng làm việc với Angular chắc hẳn đều đã quen thuộc với design pattern này).
Nếu bạn chưa nắm rõ phần này, bạn có thể tham khảo thêm ở link sau đây Decorators trong Typescript
Mục đích chính của controllers là tiếp nhận các request cụ thể từ ứng dụng. Cơ chế routing sẽ chỉ định các controllers nào cần tiếp nhận và xử lý requests. Thông thường, mỗi controller sẽ có nhiều routes, mỗi routes chịu trách nhiệm xử lý các actions cụ thể.
Để khởi tạo nhanh các CRUD controllers đi kèm với bộ validation có sẵn, bạn có thể sử dụng đoạn CLI sau:
$ nest g resource your-resource-name
Routing
Bạn có thể khai báo một Controller bằng cách tạo một class kèm theo decorator @Controller()
. Chúng ta cũng có thể group các controllers lại với nhau thông qua việc chỉ định optional path cho decorator @Controller. Điều này cũng sẽ giúp bạn giảm thiểu code lặp không cần thiết.
// cats.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats'
}
}
Bạn có thể tạo một controller bằng CLI như sau
$ nest g controller your-controller-name
Decorator @Get()
được khai báo phía trên method findAll()
giúp Nest xác định và tiến hành điều hướng xử lý requests.
Bởi vì bạn đã khai báo prefix path cho controller là cats
, và bạn cũng để trống phần path trong decorator @Get()
, nên khi bạn truy cập link http://localhost:3000/cats
qua phương thức GET, Nest sẽ điều hướng xử lý tới method findAll()
mà bạn đã khai báo trong controller.
Cũng khá dễ hiểu đúng không ^_^
Theo mặc định thì method này sẽ trả về dữ liệu kèm theo Http Status Code 200. Bạn cũng có thể thay đổi chúng nếu muốn.
Thông tin Request
Trong vài trường hợp, bạn cần lấy một số thông tin chi tiết từ request phía client. Nest cung cấp cho chúng ta decorator @Req() để lấy thông tin từ client request. Biến này sẽ là Request
(nếu bạn sử dụng platform Express
), hoặc FastifyRequest
(nếu bạn sử dụng platform Fastify
),...
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller('cats')
export class CatsController {
@Get()
findAll(@Req() request: Request): string {
return 'This action returns all cats'
}
}
Request Object của Nest chứa rất nhiều thông tin, và trong hầu hết các trường hợp chúng ta cũng không cần phải lấy các thông tin này một cách thủ công. Do đó, Nest cung cấp cho chúng ta một số request decorators khác giúp chúng ta dễ dàng thao tác và xử lý hơn.
@Request()
,@Req()
==>req
@Response()
,@Res()
==>res
Lưu ý: khi sử dụng @Response()
hoặc @Res()
, bạn cần tự quản lý việc phản hồi, sẽ không làm điều đó cho bạn. Ví dụ, bạn cần gọi res.json()...
hoặc res.send()...
, nếu không hệ thống sẽ bị treo vĩnh viễn.
Hoặc bạn cũng có thể sử dụng thiết lập @Res({ passthrough: true })
. Lúc này, bạn không cần phải tự quản lý việc phản hồi, mà chỉ tập trung vào việc chỉnh sửa một số thông tin khác trên response. Phần còn lại, Nest sẽ làm giúp bạn.
@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
HTTP methods
Nest cung cấp các một số decorators giúp thiết lập routing qua HTTP methods.
@Get()
dùng để lấy dữ liệu.@Post()
cơ bản là tạo mới thông tin.@Put()
thường được dùng để cập nhật thông tin một bản ghi. Nên nhớ là nó sẽ thay thế bản ghi bằng nguyên cục data bạn truyền vào.@Patch()
cũng được dùng để cập nhật thông tin. Nhưng khác ở chỗ, nó chỉ cập nhật một vài fields được yêu cầu thay vì toàn bộ.@Delete()
xóa dữ liệu.@All()
phương thức này chấp nhận mọi HTTP methods.
Ngoài ra còn có @Options()
, @Head()
... nhưng mình hầu như chưa sử dụng ^_^.
HTTP Status Code
Theo mặc định, repsonse code luôn luôn là 200, đối với các request POST là 201. Bạn có thể thay đổi nó một cách dễ dàng thông qua decorator @HttpCode(...)
.
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat'
}
Đôi lúc, response code của bạn không cố định, mà phụ thuộc vào các yếu tố khác nhau. Trong trường hợp đó, bạn có thể chỉ định chúng một cách dynamic thông qua @Res()
.
Bạn cần import decorator HttpCode
từ package @nestjs/common
.
Headers
Để tùy chỉnh một Header, bạn có thể dùng decorator @Header()
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat'
}
Hoặc thông qua @Res()
@Post()
create(@Res() res) {
res
.header('Cache-Control', 'none')
.send('This action adds a new cat')
}
Bạn có thể import decorator Header
từ package @nestjs/common
.
Redirect
Để redirect tới một URL cụ thể, bạn có thể sử dụng decorator @Redirect()
hoặc thông qua @Res()
, sau đó call trực tiếp res.redirect()
.
Decorator @Redirect()
nhận 2 optional params là url: string
và statusCode: number
. Giá trị mặc định của statusCode
là 302.
@Get()
@Redirect('https://duypt.dev', 301)
Để thực hiện redirect một cách dynamic, bạn có thể sử dụng cách sau
@Get('docs')
@Redirect('https://docs.nestjs.com', 301)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: 'https://docs.nestjs.com/v5/', statusCode: 301 }
}
}
Giá trị trả về sẽ override giá trị bạn truyền vào decorator @Redirect()
.
Route parameters
Khai báo routes với path tĩnh sẽ không hoạt động khi bạn cần truyền dynamic data trực tiếp vào URL.
Để khai báo một route chấp nhận dynamic params, bạn có thể làm như sau
@Get(':id')
findOne(@Param('id') id): string {
return `This action returns a #${id} cat`
}
Nếu bạn không khai báo name cho decorator @Param()
, nó sẽ trả về toàn bộ route params nếu tồn tại.
Bạn cần import decorator Param
từ package @nestjs/common
.
Request scopes
Đối với một số bạn có nền tảng kiến thức từ một số ngôn ngữ lập trình khác (như PHP...), có thể họ sẽ khá bất ngờ khi mà hầu hết mọi thứ đều được "xài chung" trong các requests. Chúng ta có một cái connection tới database, một số singleton services được dùng toàn cục...
Hãy nhớ rằng, NodeJS không tuân theo mô hình Multi-Threaded Stateless Model (nơi mà mỗi yêu cầu được xử lý trên một thread riêng biệt). Do đó, việc sử dụng singleton hoàn toàn an toàn với Nest.
Tuy nhiên, đôi lúc bạn mong muốn một số service chỉ hoạt động trong phạm vi một request cụ thể nào đó để tránh việc chồng chéo lên nhau. Đó là lúc bạn cần quan tâm tới request scopes.
Mình sẽ đi sâu hơn về phần này trong các phần tiếp theo.
Bất đồng bộ (Asynchronicity)
Hầu hết các công việc thao tác với dữ liệu hoặc hệ thống khác đều là bất đồng bộ. NestJS cũng hỗ trợ và hoạt động rất tốt với async
.
Tất cả các async functions đều trả về một Promise. Nest sẽ tự giải quyết phần xử lý cho bạn.
@Get()
async findAll(): Promise<any[]> {
return []
}
Ngoài ra, Nest còn hỗ trợ một framework xử lý bất đồng bộ cực kỳ mạnh mẽ - RxJS theo cơ chế Observable streams hiện đại. Nest sẽ tự động subscribe để lấy về giá trị cuối cùng khi stream completed.
@Get()
findAll(): Observable<any[]> {
return of([])
}
Bạn sử dụng kiểu nào cũng được, nhưng cá nhân mình đề nghị bạn dùng Observable nhé ^_^. Cứ xài thử và bạn sẽ mê mẩn ngay kkk.
Mình sẽ viết một vài bài chi tiết về RxJS sau nhé ^_^.
Truyền nhận dữ liệu qua DTO
Nest hỗ trợ chúng ta đọc dữ liệu truyền lên từ client request thông qua decorator @Body()
.
Bạn cần lưu ý là theo chuẩn HTTP, các requests sử dụng method POST, PUT, PATCH mới có thể đính kèm payload, do đó decorator @Body()
chỉ hoạt động với các HTTP methods này.
DTO (Data Transfer Object) - là cách triển khai một design pattern rất phổ biến - Transfer Object Pattern. Chúng đơn giản là các object xác định kiểu dữ liệu sẽ được gửi đi hoặc nhận về, và có thể được serialize khi truyền qua mạng. Chúng không - và cũng không nên có các logic xử lý bên dưới.
Nest hỗ trợ chúng ta khai báo DTO qua interface hoặc các class. Tuy nhiên, mình khuyên các bạn hãy sử dụng class.
- Class là một phần trong chuẩn Javascript ES6. Do đó, chúng được giữ lại sau khi compiled.
- Trong khi đó interface chỉ là cú pháp của Typescript, chúng không tồn tại, và cũng sẽ bị xóa khỏi code Javascript sau khi compiled. Do đó, Nest không thể tham chiếu đến chúng trong lúc chạy.
- Thêm một vài điểm nhấn quan trọng khi bạn sử dụng class:
- Rất dễ dàng thực hiện validate dữ liệu đầu cuối thông qua class-validator.
- Có thể transform dữ liệu thông qua các Pipes. Bởi class là tồn tại, do đó chúng ta có thể can thiệp vào các metadata của chúng trong thời gian chạy.
//create-cat.dto.ts
export class CreateCatDto {
name: string
age: number
breed: string
}
//cats.controller.ts
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat'
}
Bạn cũng có thể thêm một vài validation như thế này
//create-cat.dto.ts
export class CreateCatDto {
@IsNotEmpty()
name: string
@IsInt()
@Optional()
age: number
@MinLength(5)
breed: string
}
Vừa dễ lại vừa clean đúng không ^_^.
Xử lý Exceptions
Nest hỗ trợ bạn rất nhiều trong việc xử lý Exceptions. Mình sẽ trình bày thêm phần này trong các bài tiếp theo nhé ^_^
Kết
Mình xin phép kết thúc phần giới thiệu cơ bản về controllers ở đây. Hẹn gặp lại các bạn trong các bài tiếp theo nhé.