Skip to main content

controller (03,04)

在 Nest 的世界裡,Controller 負責路由的配置並處理來自客戶端的請求,而每一個 Controller 都可以依照需求來設計不同 Http Method 的資源,就好像外場服務生負責帶位、協助客人點餐一樣,並根據客戶的需求做出相對應的回應:

總體來說,Controller 就是一個處理客戶端請求,並將相同性質的資源整合在一起的元件。

建置 Controller

所有的 Controller 都必須使用 @Controller 裝飾器來定義。可以用 NestCLI 快速生成 Controller:

$ nest generate controller <CONTROLLER_NAME>

注意:<CONTROLLER_NAME> 可以含有路徑,如:features/todo,這樣就會在 src 資料夾下建立該路徑並含有 Controller。

這邊我建立了一個名為 todo 的 Controller:

$ nest generate controller todo

src 底下會看見一個名為 todo 的資料夾,裡面有 todo.controller.ts 以及 todo.controller.spec.ts

由於只有建立 Controller,所以會自動將其歸納於根模組下,也就是說 AppModule 會多引入 TodoController

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoController } from './todo/todo.controller';

@Module({
imports: [],
controllers: [AppController, TodoController],
providers: [AppService],
})
export class AppModule {}

路由

在建置完 Controller 基本骨架後,會發現 todo.controller.ts@Controller 多了一個字串 todo,這是路由的 前綴 (prefix)

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

@Controller('todo')
export class TodoController {}

注意:透過 NestCLI 建立的 Controller 前綴預設使用該 Controller 的名稱,通常會習慣把名稱取單數,而前綴改為複數。

添加路由前綴的好處是可以使相同路由的資源都歸納在同一個 Controller 裡面,其中包含了前綴底下的所有子路由:

Http Methods

可以透過添加裝飾器在 class 的方法上,來指定不同 Http Method 所呼叫的方法,Nest 會根據 Controller 與指定的 Http Method 裝飾器來建立路由表。以下方程式碼為例,使用的為 GET

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

@Controller('todos')
export class TodoController {
@Get()
getAll() {
return [];
}
}

使用瀏覽器進入 http://localhost:3000/todos 來查看結果:

!https://ithelp.ithome.com.tw/upload/images/20210321/20119338AV8qBUugaH.png

Nest 的 Http Method 裝飾器名稱即對應標準 Http Method,這裡做了些歸納:

  • @Get:表示接收對應路由且為 GET 請求時觸發。
  • @Post:表示接收對應路由且為 POST 請求時觸發。
  • @Put:表示接收對應路由且為 PUT 請求時觸發。
  • @Patch:表示接收對應路由且為 PATCH 請求時觸發。
  • @Delete:表示接收對應路由且為 DELETE 請求時觸發。
  • @Options:表示接收對應路由且為 OPTIONS 請求時觸發。
  • @Head:表示接收對應路由且為 HEAD 請求時觸發。
  • @All:表示接收對應路由且為以上任何方式的請求時觸發。

子路由

在設計路由時,很有可能會有子路由的需求,比如說:/todos 底下還有一個取得範例的資源,定義為 GET /todos/examples,但不可能每次有子路由都建立一個新的 Controller,這時候就可以透過 Http Method 裝飾器指定子路由,會基於 Controller 設置的前綴來建立,以下方程式碼為例,取得資源的路由為 GET /todos/examples

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

@Controller('todos')
export class TodoController {
@Get('/examples')
getExample() {
return [
{
id: 1,
title: 'Example 1',
description: ''
}
];
}
}

使用瀏覽器進入 http://localhost:3000/todos/examples 查看結果:

通用路由符號

注意:該用法使用上需要謹慎,基本上除了 ? 以外,都是被允許的字元。

有時候設計路由時,可能會提供些許的容錯空間,比如說:原本是 GET /todos/examples,但不管是 /todos/exammmmmmmmples 還是 /todos/exam_ples 都可以得到 /todos/examples 的結果,主要是在指定路由時,使用了 *。下方為範例程式碼:

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

@Controller('todos')
export class TodoController {
@Get('exam*ples')
get() {
return [
{
id: 1,
title: 'Example 1',
description: ''
}
];
}
}

使用瀏覽器進入 http://localhost:3000/todos/exammmmmmmmples 查看結果:

路由參數 (Path Parameters)

路由參數的設計十分簡單,會在 Http Method 裝飾器上做定義,字串格式為 :<PARAMETER_NAME>,接著要在該方法中添加帶有 @Param 裝飾器的參數,這樣就可以順利取得路由參數。這裡我們新增一個路由參數為 id 的路由,程式碼如下:

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

@Controller('todos')
export class TodoController {
@Get(':id')
get(@Param() params: { id: string }) {
const { id } = params;
return {
id,
title: `Title ${id}`,
description: ''
};
}
}

這裡還有另一種方式可以取得特定路由參數,就是在 @Param 帶入指定參數名稱:

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

@Controller('todos')
export class TodoController {
@Get(':id')
get(@Param('id') id: string) {
return {
id,
title: `Title ${id}`,
description: ''
};
}
}

使用瀏覽器進入 http://localhost:3000/todos/1 查看結果:

查詢參數 (Query Parameters)

查詢參數與路由參數取得的方式很相似,但不需要在 Http Method 裝飾器中做任何設置,只需要在方法中添加帶有 @Query 的參數即可。這裡我們做一個簡單的範例:

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

@Controller('todos')
export class TodoController {
@Get()
getList(@Query() query: { limit: number, skip: number }) {
const { limit = 30, skip = 0 } = query;
const list = [
{
id: 1,
title: 'Title 1',
description: ''
},
{
id: 2,
title: 'Title 2',
description: ''
}
];

return list.slice(skip, limit);
}

}

也可以取得特定查詢參數,就是在 @Query 帶入指定參數名稱:

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

@Controller('todos')
export class TodoController {
@Get()
getList(
@Query('limit') limit: number = 30,
@Query('skip') skip: number = 0
) {
const list = [
{
id: 1,
title: 'Title 1',
description: ''
},
{
id: 2,
title: 'Title 2',
description: ''
}
];

return list.slice(skip, limit);
}
}

使用瀏覽器進入 http://localhost:3000/todos?limit=1 查看結果:

狀態碼 (Http Code)

預設情況下,除了 POST 會回傳 201 外,大多數的 Http Method 都是回傳 200,不過應該要以實際情況來回傳適當的狀態碼。Nest 提供了狀態碼的 enum,並用裝飾器來設置回傳的狀態碼,十分貼心!下方為範例程式碼:

import { Controller, Patch, HttpCode, HttpStatus } from '@nestjs/common';

@Controller('todos')
export class TodoController {
@Patch()
@HttpCode(HttpStatus.NO_CONTENT)
get() {
return [];
}
}

這裡用 Postman 來查看結果:

小結

今天的內容稍微多了些,這邊幫大家懶人包一下:

  1. Controller 負責路由及處理來自客戶端的請求。
  2. Controller 可以將相同路徑下的資源整合在一起,包含子路由。
  3. 透過 Http Method 裝飾器輕鬆配置對應的資源。
  4. 透過 @Param 取得路由參數、透過 @Query 取得查詢參數。
  5. 透過 @HttpCodeHttpStatus 來配置該資源回傳的狀態碼。

Controller 的功能非常多,如果都放在同一篇的話篇幅會過長,所以剩下的部分會在下一篇繼續說明。

主體資料 (Body)

在傳輸資料時經常會使用到主體資料,比如說:POSTPUTPATCH等操作,Nest 有提供 @Body 裝飾器來取得主體資料。範例程式碼如下:

import { Body, Controller, Post } from "@nestjs/common";

@Controller("todos")
export class TodoController {
@Post()
create(@Body() data: { title: string; description?: string }) {
const id = 1;
return { id, ...data };
}
}

也可以透過指定參數名稱來取得特定參數:

import { Body, Controller, Post } from "@nestjs/common";

@Controller("todos")
export class TodoController {
@Post()
create(@Body("title") title: string, @Body("description") description?: string) {
const id = 1;
return { id, title, description };
}
}

透過 Postman 查看結果: https://ithelp.ithome.com.tw/upload/images/20210308/20119338lL5jz3GadJ.png

使用 DTO

什麼是 DTO?它的全名是 資料傳輸物件 (Data Transfer Object),其用途廣泛,通常用於過濾、格式化資料,它只負責存放要傳遞的資訊,故 只有唯讀屬性,沒有任何方法。定義 DTO 之後,就不必一直翻文件查到底參數格式為何,可以很清楚了解傳入 / 傳出的參數內容,在 Nest 的世界裡,甚至可以基於 DTO 的格式來設置驗證器,進而大幅降低維護成本。

既然是定義格式,那麼就有兩種選擇:

  1. TypeScript 的 interface
  2. 標準 JavaScript 支援的 class

基本上會建議大家採用 class 的形式來建立 DTO,原因是 interface 在編譯成 JavaScript 就會被刪除,而 class 會保留,這對部分功能是有影響的,所以 官方也推薦大家採用 class

那麼就來建立一個範例的 DTO 吧,在要調整的 Controller 目錄下,新增一個名為 dto 的資料夾,並建立 create-<CONTROLLER_NAME>.dto.ts,我這邊的檔案名稱為 create-todo.dto.ts

export class CreateTodoDto {
public readonly title: string;
public readonly description?: string;
}

建立完畢後,在 Controller 中使用,將帶有 @Body 裝飾器之參數的型別指定為該 DTO:

import { Body, Controller, Post } from "@nestjs/common";
import { CreateTodoDto } from "./dto/create-todo.dto";

@Controller("todos")
export class TodoController {
@Post()
create(@Body() dto: CreateTodoDto) {
const id = 1;
return { id, ...dto };
}
}

標頭 (Headers)

有時候可能需要設置標頭來回傳給用戶端,這時候就可以用 @Header 裝飾器來配置:

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

@Controller("todos")
export class TodoController {
@Get()
@Header("X-Hao-headers", "1")
getAll() {
return {
id: 1,
title: "Title 1",
description: "",
};
}
}

透過 Postman 查看結果: https://ithelp.ithome.com.tw/upload/images/20210308/20119338Yo5Ha9PFjF.png

參數裝飾器

前面有提過 Nest 是以 Express 或 Fastify 作為底層基礎進行整合的框架,在很多地方都是對底層平台進行包裝的,其中的參數正是包裝出來的,透過特定的參數裝飾器來取得不同的資訊,除了前面提及的幾項以外,還有提供許多參數裝飾器來提供開發人員取得更多資訊:

  • @Request():請求的裝飾器,帶有此裝飾器的參數會賦予底層框架的 請求物件 (Request Object),該裝飾器有別稱 @Req(),通常將參數名稱取為 req
  • @Response():回應的裝飾器,帶有此裝飾器的參數會賦予底層框架的 回應物件 (Response Object),該裝飾器有別稱 @Res(),通常將參數名稱取為 res
  • @Next():Next 函式的裝飾器,帶有此裝飾器的參數會賦予底層框架的 Next 函式,用途為呼叫下一個 中介軟體 (Middleware),詳細說明可以參考我先前寫的 Express 基本結構與路由
  • @Param(key?: string):路由參數的裝飾器,相當於 req.paramsreq.params[key]
  • @Query(key?: string):查詢參數的裝飾器,相當於 req.queryreq.query[key]
  • @Body(key?: string):主體資料的裝飾器,相當於 req.bodyreq.body[key]
  • @Headers(name?: string):請求標頭的裝飾器,相當於 req.headersreq.headers[name]
  • @Session():session 的裝飾器,相當於 req.session
  • @Ip():IP 的裝飾器,相當於 req.ip
  • @HostParam():host 的裝飾器,相當於 req.hosts

處理回應的方式

前面我們使用的範例都是透過 return 的方式來處理回應資料的,事實上 Nest 提供了兩種方式來處理回應:

標準模式

透過 return 資料的方式讓 Nest 來處理回應的動作,也是 官方最推薦 的方式。範例程式碼如下:

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

@Controller("todos")
export class TodoController {
@Get()
getAll() {
return [];
}
}

非同步

在後端領域中,幾乎都會使用到非同步操作,這時候用 ES7 的 async/await 再好不過,而標準模式也支援此方式:

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

@Controller("todos")
export class TodoController {
@Get()
async getAll() {
return new Promise((resolve, reject) => setTimeout(() => resolve([]), 1000));
}
}

RxJS

RxJS 也是近年來十分熱門的函式庫,在 Angular 中可以經常看到它的身影,而受到 Angular 啟發的 Nest 也跟進使用了 RxJS。Nest 會自動訂閱 / 取消訂閱對象,無須手動取消訂閱,下方為範例程式碼:

import { Controller, Get } from "@nestjs/common";
import { of } from "rxjs";

@Controller("todos")
export class TodoController {
@Get()
getAll() {
return of([]);
}
}

函式庫模式

這個模式是使用底層框架的回應物件來處理回應,不透過 return 的方式讓 Nest 處理,相當於每個方法都是 void。下方為範例程式碼:

import { Controller, Get, Res } from "@nestjs/common";
import { Response } from "express";

@Controller("todos")
export class TodoController {
@Get()
getAll(@Res() res: Response) {
res.send([]);
}
}

注意:須依照使用的底層框架來決定 res 的型別,範例中使用 Express 作為底層,故用其 Response 型別。

模式的限制

Nest 會去偵測是否有帶 @Res@Response@Next 裝飾器的參數,如果有的話,該資源就會啟用函式庫模式,而標準模式會被關閉,這是什麼意思呢?簡單來說 return 值的方式會失去作用。下方為範例程式碼:

import { Controller, Get, Res } from "@nestjs/common";
import { Response } from "express";

@Controller("todos")
export class TodoController {
@Get()
getAll(@Res() res: Response) {
return [];
}
}

可以發現按照此段程式碼建構的資源無法被存取: https://ithelp.ithome.com.tw/upload/images/20210310/20119338R2rqwkDNVd.png

那如果真的要從回應物件中取得資訊,但又想採用標準模式的話,有什麼方法可以突破此限制嗎?答案是有的,只需要在裝飾器中添加 passthrough: true 即可:

import { Controller, Get, Res } from "@nestjs/common";
import { Response } from "express";

@Controller("todos")
export class TodoController {
@Get()
getAll(@Res({ passthrough: true }) res: Response) {
return [];
}
}

透過 Postman 查看結果: https://ithelp.ithome.com.tw/upload/images/20210310/20119338wxgtDeLroL.png

小結

先來懶人包一下今天的內容:

  1. 透過 @Body 取得主體資料。
  2. 善用 DTO 來定義資料傳輸格式。
  3. 透過 @Header 配置回應的標頭。
  4. 了解 Nest 提供的參數裝飾器個別的功能。
  5. 用標準模式就是使用 return 的方式回傳資訊,並支援 async/awaitRxJS
  6. 函式庫模式透過底層框架的回應物件來回傳資訊。
  7. 使用 @Response@Res@Next 會關閉標準模式,若要在標準模式下使用的話需要添加 passthrough: true 在裝飾器中。

Controller 負責處理客戶端的請求,可以把它視為 API 的門面,既然是門面,那麼在面對各式各樣的請求都要有相對應的應對方式,所以 Controller 的功能這麼多合情合理!

還記得前面說過的 Controller 是某個區塊的外場服務生嗎?目前的範例是將 Controller 直接掛在根模組下,如果所有的 Controller 都掛在根模組的話,會變成所有的外場服務生都屬於同一區,並沒有做到良好的動線規劃,所以應該要將 Controller 放入對應的 Module。下一篇將會介紹 Nest 中的 Module,那就明天見囉!