Tổng quan – NestJS

Controllers

Controllers có trách nhiệm xử lý các request gửi đến và trả lại response cho client.

Mục đích của controller là nhận các request cụ thể cho ứng dụng. Cơ chế định tuyến (route) kiểm soát controller nào nhận được request nào. Thông thường, mỗi controller có nhiều hơn một định tuyến (route) và các định tuyến khác nhau có thể thực hiện các hành động khác nhau.

Để tạo một controller cơ bản, chúng ta sử dụng các class và các decorator. Decorator liên kết với cácclass với metadata bắt buộc và cho phép Nest tạo ra bản đồ định tuyến (route map) (liên kết các request với controller tương ứng).

Gợi ý: Để tạo nhanh một controller CRUD với validation tích hợp sẵn, bạn có thể sử dụng trình tạo CRUD của CLI:  nest g resource [name].

Routing

Trong ví dụ dưới đây, ta sử dụng @Controller() decorator, nó được yêu cầu để định nghĩa một controller cơ bản. Ta sẽ chỉ định tiền tố đường dẫn tuỳ chọn của cats. Sử dụng tiền tố đường dẫn trong một @Controller() decorator cho phép chúng ta dễ ràng nhóm một tập các route liên quan, giảm thiểu đoạn mã lặp. Ví dụ, ta có thể chọn nhóm một tập hợp các route mà nó quản lý tương tác với entity customer theo route /customers. Trong trường hợp đó, ta có thể chỉ định tiền tố customers trong @Controller() decorator để ta không phải lặp lại đường dẫn cho mỗi route trong file.

cats.controller.ts

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

Gợi ý: Để tạo một controller sử dụng CLI, thực thi lệnh  $ nest g controller cats.

Phương thức @Get() HTTP request decorator phía trước phương thức findAll() yêu cầu Nest tạo một handler cho một endpoint cụ thể cho các request HTTP. Endpoint tương ứng với HTTP request (GET trong trường hợp này) và đường dẫn route. Đường dẫn route là gì? Đường dẫn route cho một handler được xác định bằng cách nối tiền tố (tuỳ chọn) được khai báo cho controller và bất kỳ đường dẫn xác định trong một request decorator. Vì ta đã khai báo tiền tố cho mọi route (cats), và chưa thêm bất kỳ thông tin đường dẫn nào vào trong decorator, Nest sẽ map các request GET /cats đến handler này. Như đã đề cập, đường dẫn bao gồm cả tiền tố đường dẫn controller (tuỳ chọn) bất kỳ chuỗi đường dẫn nào được khai báo trong phương thức request decorator. Ví dụ, một tiền tố đường dẫn của customers được kết hợp với @Get(‘profile’) decorator sẽ tạo ra một ánh xạ route cho các request như GET /customers/profile.

Trong ví dụ trên, khi một GET request được thực hiện tới endpoint này, Nest sẽ route request đến các phương thức findAll() do người dùng định nghĩa. Lưu ý rằng tên các phương thức ta chọn ở đây là hoàn toàn tuỳ ý. Rõ ràng là ta phải khai báo một phương thức để liên kết với route, nhưng Nest không gắn bất kỳ ý nghĩa nào với tên phương thức đã chọn.

Phương thức này trả về status code 200 và response liên quan, trong trường hợp này chỉ là một chuỗi. Tại sao điều đó xảy ra? Để giải thích, trước tiên ta sẽ giới thiệu khái niệm Nest sử dụng 2 tuỳ chọn khác nhau để điều khiển response:

Tiêu chuẩn (recommended)Sử dụng cách thức tích hợp, khi một request handler trả về một JavaScript object hoặc array, nó sẽ tự động chuyển sang dạng JSON. Khi nó trả về kiểu nguyên thuỷ JavaScript (e.g., stringnumberboolean), tuy nhiên , Nest sẽ chỉ gửi giá trị mà không cần thử chuyển đổi chúng. Điều này làm cho việc xử lý response trở nên đơn giản: chỉ cần trả về value, và Nest sẽ lo phần còn lại.

Hơn nữa, response status code là luôn luôn 200 mặc đinh, trừ khi POST requests sẽ sử dụng 201. Ta có thể dễ dàng thay đổi hành vì này bằng cách @HttpCode(...) decorator tại một handler-level (xem thêm Status codes).
Thư viện riêngTa có thể sử dụng thư viện cụ thể (e.g., Express) response object, nó có thể injected sử dụng @Res() decorator trong handler method signature (e.g., findAll(@Res() response)). Với cách tiếp cận này, bạn có khả năng sử dụng handle các phương thức gốc được hiển thị bởi đối tượng đó. Ví dụ, với Express, bạn có thể tạo response bằng cách sử dụng mã như response.status(200).send().

Cảnh báo: Nest phát hiện khi handler đang sử dụng @Res() hoặc @Next(), cho biết bạn đã chọn tùy chọn dành thư viện riêng. Nếu cả 2 cách tiếp cận được sử dụng cùng một lúc, thì cách tiếp cận Tiêu chuẩn sẽ tự động bị vô hiểu hóa cho một route duy nhất này và sẽ không còn hoạt động như mong đợi, bạn phải đặt passthrough tùy chọn thành true trong @Res({ passthrough: true }) decorator.

Request object

Handler thường cần truy cập vào các chi tiết request của client. Nest cung cấp quyền truy cập vào các request object của platform bên dưới (Express mặc định). Ta có thể truy cập vào request object bằng cách cung cấp Nest để inject nó bằng cách thêm @Req() decorator vào signature của handler.

cats.controller.ts

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';
  }
}

Gợi ý: Để tận dụng các kiểu gõ express (như trong request: Request parameter ví dụ phía trên), hay cài đặt gói @types/express.

Request object đại diện cho request HTTP và các thuộc tính của chuỗi query, request, parameters, HTTP headers, và body (đọc thêm tại đây). Trong hầu hết các trường hợp, không cần thiết phải lấy các thuộc tính theo cách thủ công. Thay vào đó, ta có thể sử dụng các decorator chuyên dụng, chẳng hạn như @Body() hoặc @Query(), sẵn có dùng được luôn. Dưới đây là danh sách các decorator được cung cấp và các đối tượng đơn giản dành riêng cho platform 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 với các kiểu chữ trên platform HTTP (eg, Express và Fastify), Nest cung cấp @Res()@Response() decorator. @Res() đơn giản là viết tắt cho @Response(). Cả 2 đều trực tiếp hiển thị giao diện Object response trên nền tảng gốc bên dưới. Khi sử dụng chúng, bạn cũng nên nhập các kiểu chữ cho thư viện dưới (ví dụ: @type/express) để tận dụng tối đa. Lưu ý rằng khi bạn inject @Res() hoặc @Response() vào phương thức handler, bạn đặt Nest vào chế độ dành riêng cho thư viện cho handler đó và bạn chịu trách nhiệm quản lý response. Khi làm như vậy, bạn phải đưa ra một số loại response bằng cách gọi response object (eg, res.json(…) hoặc res.send(…) ), hoặc nếu không máy chủ HTTP sẽ bị treo.

HINT: Để tìm hiểu cách tạo tùy chỉnh decorators, xem chương này.

Resources

Trước đó, ta đã xác định được một endpoint để tìm resource cats (GET route). Thông thường, ta cũng muốn cung cấp một endpoint để tạo các bản ghi mới. Đối với điều này, hãy tạo POST handler:

cats.controller.ts

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';
  }
}

Điều này đơn giản, Nest cung cấp decorator cho tất cả các phương thức HTTP tiêu chuẩn: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), and @Head(). Ngoài ra, @All() xác định một endpoint – handle tất cả chúng.

Route wildcards

Pattern dựa trên route cũng được hỗ trợ. Ví dụ, dấu hình sao (*) được sử dụng làm wildcard (ký tự đại diện), và sẽ khớp với bất kỳ tổ hợp ký tự nào.

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

Đường dẫn route 'ab*cd' sẽ khớp với abcdab_cdabecd và vân vân. Những ký tự ?+*, và () có thể được sử dụng trong đường dẫn route, và là các tập con của các regular expression counterparts của chúng. Dấu gạch nối (-) và dấu chấm (.) được hiểu theo nghĩa đen bằng các đường dẫn dựa trên chuỗi.

Status code

Như đã đề cập, status code – response luôn mặc định là 200, ngoài trừ các POST request sẽ là 201. Ta có thể dễ dàng thay đổi hành vi này bằng thêm @HttpCode(…) decorator ở handler-level.

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

HINT: Nhập HttpCode từ gói @nestjs/common.

Thông thường, status code của bạn không tĩnh mà phụ thuộc vào các yếu tố khác nhau. Trong trường hợp đó, bạn có thể sử dụng một response dành riêng cho thư viện (inject sử dụng @Res()) object (hoặc, trong trường hợp lỗi, ném ra 1 exception).

Headers

Để chỉ định một tùy chỉnh response header, bạn có thể sử dụng một @Header() decorator hoặc response object thư viên riêng (và gọi trực tiếp res.header()).

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

HINT: Nhập Header từ gói @nestjs/common.

Redirection

Để redirect một response tới một URL cụ thể, bạn có thể sử dụng @Redirect() decorator hoặc một thư viện riêng response object (và gọi trực tiếp res.redirect()).

@Redirect() nhận 2 đối số (arguments), url và statusCode, cả 2 đều là tùy chọn. Giá trị mặc định của statusCode is 302 (Found) nếu bị bỏ qua ko truyền gì.

@Get()
@Redirect('https://nestjs.com', 301)

Đôi khi bạn có thể muốn xác định status code HTTP hoặc URL redirect động. Thực hiện việc này bằng cách trả về một object từ phương thức route handler có hình dạng:

{
  "url": string,
  "statusCode": number
}

Các giá trị trả về sẽ ghi đè bất kỳ đối số nào được truyền đến @Redirect() decorator. 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/' };
  }
}

Route parameters

Các route với đường dẫn tĩnh sẽ không hoạt động khi bạn cần chấp nhận dữ liệu động như một phần của request (ví dụ: GET /cat/1 để nhận cat có id 1). Để xác định các route với các tham số, ta có thể thêm các token route parameter trong đường dẫn của route để nắm bắt giá trị động tại vị trí đó URL request. Token route parameter trong @Get() decorator ví dụ dưới thể hiện cách sử dụng này. Các tham số route được khai báo theo cách này có thể được truy cập bằng cách sử dụng @Param() decorator, cần được thêm vào method signature.

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Param() được sử dụng để decorate một phương thức tham số (các params trong ví dụ trên) và làm cho các tham số route có sẵn dưới dạng thuộc tính của tham số phương thức được decorated đó bên trong phần thân của phương thức. Như đã thấy đoạn mã trên, ta có thể truy cập tham số params.id. Bạn cũng có thể truyền 1 mã token cụ thể vào decorator, và sau đó tham chiếu trực tiếp tham số route theo tên trong thân phương thức.

HINT Nhập Param từ gói the @nestjs/common.

@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

Sub-Domain Routing

@Controller decorator có thể có một tùy chọn host để yêu cầu máy chủ HTTP của các request đến khớp với một số giá trị cụ thể.

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

WARNING: Vì Fastify thiếu hỗ trợ cho các route lồng nhau, khi sử dụng domain phụ định tuyến, bộ điều hợp Express (mặc đinh) nên được sử udngj thay thế.

Tương tự như một route path, tùy chọn host có thể sử dụng token để nắm bắt giá trị động tại vị trí đó trong tên máy chủ. Tham số token host trong @Controller() decorator ví dụ dưới thể hiện cách sử 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 cách sử dụng @HostParam() decorator, cần được thêm vào method signature.

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

Scopes

Đối với những người đến từ các nền tảng ngôn ngữ lập trình khác nhau, có thể bất ngờ khi biết rằng trong Nest, hầu hết mọi thứ đều được chia sẻ qua các request gửi đến. Chúng ta có một nhóm kết nối đến cơ sở dữ liệu, các singleton service với trạng thái toàn cục, v.v. Hãy nhớ rằng Node.js không tuân theo Mô hình không trạng thái đa luồng yêu cầu / phản hồi trong đó mọi request được xử lý bởi một luồng riêng biệt. Do đó, việc sử dụng các instance singleton hoàn toàn an toàn cho các ứng dụng của chúng tôi.

Tuy nhiên, có những trường hợp cạnh khi thời gian tồn tại dựa trên request của controller có thể là hành vi mong muốn, ví dụ như bộ nhớ đệm theo request trong các ứng dụng GraphQL, theo dõi request hoặc cho thuê nhiều lần. Tìm hiểu cách kiểm soát phạm vi tại đây.

Asynchronicity

Chúng tôi yêu thích JavaScript hiện đại và chúng tôi biết rằng việc trích xuất dữ liệu chủ yếu là không đồng bộ. Đó là lý do tại sao Nest hỗ trợ và hoạt động tốt với các function không đồng bộ.

HINT Tìm hiểu thêm tính năng async / await tại đây.

Mọi hàm không đồng bộ phải trả về một Promise. Điều này có nghĩa là bạn có thể trả về giá trị hoãn lại mà Nest sẽ có thể tự resolve. Hãy xem một ví dụ về điều này:

cats.controller.ts

@Get()
async findAll(): Promise<any[]> {
  return [];
}

Mã trên là hoàn toàn hợp lệ. Hơn nữa, các route hander Nest thậm chí còn mạnh mẽ hơn khi có thể trả về các  RxJS observable streams. Nest sẽ tự động subscribe nguồn bên dưới và lấy giá trị phát ra cuối cùng (sau khi quá trình phát trực tiếp hoàn tất).

cats.controller.ts

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

Cả hai cách trên đều hoạt động và bạn có thể sử dụng bất cứ cách nào phù hợp với yêu cầu của mình.

Request payloads

Ví dụ trước đây của chúng tôi về handler route POST không chấp nhận bất kỳ client params. Hãy sửa lỗi này bằng cách thêm @Body() decorator vào đây.

Nhưng trước tiên (nếu bạn sử dụng TypeScript), chúng ta cần xác định lược đồ DTO (Đối tượng truyền dữ liệu). DTO là một đối tượng xác định cách dữ liệu sẽ được gửi qua mạng. Chúng tôi có thể xác định lược đồ DTO bằng cách sử dụng các interface TypeScript hoặc bằng các class đơn giản. Thật thú vị, chúng tôi khuyên bạn nên sử dụng các class ở đây. Tại sao? Các lớp là một phần của tiêu chuẩn JavaScript ES6, và do đó chúng được giữ nguyên như các thực thể thực trong JavaScript đã biên dịch. Mặt khác, vì các interfaces TypeScript bị xóa trong quá trình chuyển đổi, Nest không thể tham chiếu đến chúng trong thời gian chạy. Điều này rất quan trọng vì các tính năng như Pipes cho phép các khả năng bổ sung khi chúng có quyền truy cập vào metatype của biến trong runtime.

Hãy tạo lớp CreateCatDto:

create-cat.dto.ts

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

Nó chỉ có 3 thuộc tính cơ bản. Sau đó, chúng ta có thể sử dụng DTO mới được tạo bên trong CatsController:

cats.controller.ts

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

Handling errors

Có một chương riêng về xử lý lỗi (tức là làm việc với các ngoại lệ) ở đây.

Full resource sample

Dưới đây là một ví dụ sử dụng một số decorator có sẵn để tạo một controller cơ bản. Controller này đưa ra một số phương pháp để truy cập và thao tác dữ liệu nội bộ.

cats.controller.ts


import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@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`;
  }
}

HINT Nest CLI cung cấp một trình tạo (sơ đồ) tạo tất cả mã soạn sẵn để giúp chung tôi tránh làm tất cả những điều này và làm cho trải nghiệm của nhà phát triển trở nên đơn giản hơn nhiều. Đọc thêm về tính năng này.

Getting up and running

Với controller ở trên được xác định đầy đủ, Nest vẫn không biết rằng CatsController tồn tại và kết quả là sẽ không tạo một instance của lớp này.

Controller luôn thuộc về một module, đó là lý do tại sao chúng tôi bao gồm mảng controllers trong @Module() decorator. Vì chúng tôi chưa xác định bất kỳ module nào khác ngoại trừ AppModule gốc, chúng tôi sẽ sử dụng module đó để giới thiệu CatsController:

app.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

Chúng tôi đã đính kèm metadata vào lớp module bằng cách sử dụng @Module() decorator và Nest hiện có thể dễ dàng phản ánh controller nào cần được gắn kết.

Library-specific approach

Cho đến nay, chúng ta đã thảo luận về cách thao tác response tiêu chuẩn của Nest. Cách thứ hai để điều khiển response là sử dụng thư viên riêng response object. Để chèn một response object, cụ thể, chúng ta cần sử dụng @Res() decorator. Để hiển thị sự khác biệt, hãy viết lại CatsController thành 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([]);
  }
}

Mặc dù cách tiếp cận này hoạt động và trên thực tế cho phép linh hoạt hơn theo một số cách bằng cách cung cấp toàn quyền kiểm soát response object (thao tác tiêu đề, các tính năng cho thư viện riêng, v.v.), nó nên được sử dụng cẩn thận. Nhìn chung, cách tiếp cận này kém rõ ràng hơn nhiều và có một số nhược điểm. Bất lợi chính là mã của bạn trở nên phụ thuộc vào nền tảng (vì các thư viện cơ bản có thể có các API khác nhau trên response object) và khó kiểm tra hơn (bạn sẽ phải mô phỏng response object, v.v.).

Ngoài ra, trong ví dụ trên, bạn mất khả năng tương thích với các tính năng Nest phụ thuộc vào việc xử lý response tiêu chuẩn của Nest, chẳng hạn như Interceptiors và @HttpCode() / @Header() decorator. Để khắc phục điều này, bạn có thể đặt passthrough tùy chọn thành true, như sau:

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

Bây giờ bạn có thể tương tác với response object gốc (ví dụ: đặt cookie hoặc tiêu đề tùy thuộc vào các điều kiện nhất định), nhưng hãy để phần còn lại cho framework.

(Nguồn https://docs.nestjs.com/controllers)

One Comment on “Tổng quan – NestJS

  1. Nếu một interceptor handle() được gọi ở bất kỳ đâu trên đường đi, phương thức create() sẽ không được thực thi. => Nếu một interceptor handle() “”không”” được gọi ở bất kỳ đâu trên đường đi, phương thức create() sẽ không được thực thi.

Leave a Reply

%d bloggers like this: