控制器揭秘:掌控 HTTP 請求的藝術 === ## 控制器 (Controller) 控制器的目的是接收應用程式中所有 HTTP 請求和響應。路由機制控制哪個控制器處理哪個請求。通常,每個控制器都有一個或多個路由,每個路由都可以有若干個相關聯的處理程序。 ### 建置控制器 所有控制器都必須使用 `@Controller()` 裝飾器來定義,使用 NestCLI 建置控制器的方式如下: ```bash nest g controller <CONTROLLER_NAME> ``` 這邊以建立 `users` 控制器為例: ```bash nest g controller users ``` 建置完成後,會在 `src` 資料夾下產生一個新的目錄,裡面會包含 `users.controller.ts` 與 `users.controller.spec.ts` 兩個檔案。 同時,`app.module.ts` 會自動注入 `UsersController` 到模組中。 ```typescript import { Module } from "@nestjs/common"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; import { UsersController } from "./users/users.controller"; @Module({ imports: [], controllers: [AppController, UsersController], providers: [AppService], }) export class AppModule {} ``` 在建置完成後,就可以在 `src/users/users.controller.ts` 中看到 `UsersController` 的定義。 ```typescript @Controller("users") export class UsersController {} ``` #### 路由 (Route) 會發現 `UsersController` 的裝飾器 `@Controller` 帶有一個參數 `"users"`,這是定義該控制器 **路由** 的基礎路徑,也就是所有使用該控制器的方法都會加上這個路徑前綴(prefix),並可以透過瀏覽器訪問 <http://localhost:3000/users> 來查看。 接下來,就可以在 `UsersController` 中定義各種 HTTP 方法的路由。Nest 會根據指定的 HTTP 方法裝飾器來建立路由,包含許多裝飾器,如:`@Get()`、`@Post()`、`@Put()`、`@Delete()` 等,這邊以 `@Get()` 為例: ```typescript @Controller("users") export class UsersController { @Get() getUsers(): string { return "users"; } } ``` 這時使用瀏覽器訪問 <http://localhost:3000/users>,就可以看到 `getUsers` 方法的回傳值。 Nest 的 Http Method 裝飾器名稱即對應標準 Http Method,這裡做了些歸納: - `@Get`:表示接收對應路由且為 GET 請求時觸發。 - `@Post`:表示接收對應路由且為 POST 請求時觸發。 - `@Put`:表示接收對應路由且為 PUT 請求時觸發。 - `@Patch`:表示接收對應路由且為 PATCH 請求時觸發。 - `@Delete`:表示接收對應路由且為 DELETE 請求時觸發。 - `@Options`:表示接收對應路由且為 OPTIONS 請求時觸發。 - `@Head`:表示接收對應路由且為 HEAD 請求時觸發。 - `@All`:表示接收對應路由且為以上任何方式的請求時觸發。 當請求到達對應的路由時,Nest 提供了兩種處理響應的方式: 1. **標準方式(推薦)**: - 當返回 JavaScript 物件或陣列時,會自動序列化為 JSON - 當返回基本型別(如字串、數字、布林值)時,會直接發送該值 - 預設響應狀態碼為 200(POST 請求例外,為 201) - 可以使用 `@HttpCode()` 裝飾器修改狀態碼 2. **特定框架方式**: - 可以注入特定框架(如 Express)的響應物件,使用 `@Res()` 裝飾器 - 可以使用框架原生的響應處理方法,如 `response.status(200).send()` :::warning 如果在同一個路由中同時使用了 `@Res()` 或 `@Next()`,標準方式會被自動停用。如果需要同時使用兩種方式(例如,只想設置 cookies/headers 但保留其他標準處理),可以使用 `@Res({ passthrough: true })` 來實現。 ::: #### 子路由 (Sub Route) 在設計路由時,有時會需要更細緻的路由規劃,這時就可以使用子路由(sub route),在控制器的方法上加上子路由的路徑前綴。例如,在 `/users` 路由下,再新增一個 `/profile` 路由,就可以在 `UsersController` 中定義一個 `getProfile` 方法: ```typescript @Controller("users") export class UsersController { @Get("profile") getProfile(): string { return "profile"; } } ``` 這時使用瀏覽器訪問 <http://localhost:3000/users/profile>,就可以看到 `getProfile` 方法的回傳值。 #### 通用路由符號 有時候設計路由時,可能會提供些許彈性,例如,原本是 `GET /users/profile` ,但不管是 `/users/profile` 或 `/users/proffffile` 都會對應到同一個方法,這時就可以使用通用路由符號來處理。 ```typescript @Controller("users") export class UsersController { @Get("prof*ile") getUserProfile() { return [ { id: 1, name: "John Doe", email: "john@doe.com", }, ]; } } ``` :::warning 僅 express 支援通用路由符號。 ::: #### 路由參數 (Route Parameters) 路由參數的設計十分簡單,會在 HTTP 方法裝飾器上做定義,字串格式為 `:<PARAMETER_NAME>`,接著要在該方法中添加帶有 `@Param()` 裝飾器的參數,這樣就可以順利取得路由參數。這裡我們新增一個路由參數為 `id` 的路由,程式碼如下: ```typescript @Controller("users") export class UsersController { @Get("profile/:id") getUserProfileById(@Param("id") id: string) { return { id: id, name: "John Doe", email: "john@doe.com", }; } } ``` 這時使用瀏覽器訪問 <http://localhost:3000/users/profile/1>,就可以看到 `getUserProfileById` 方法的回傳值。 #### 查詢參數 (Query Parameters) 查詢參數與路由參數取得的方式很相似,但不需要在 HTTP 方法裝飾器中做任何設置,只需要在方法中添加帶有 `@Query()` 的參數即可。這裡我們做一個簡單的範例: ```typescript @Controller("users") export class UsersController { @Get("profile") getUserProfile(@Query() query: { name: string; age: number }) { const { name, age } = query; return { name, age, }; } } ``` 這時使用瀏覽器訪問 <http://localhost:3000/users/profile?name=John&age=20>,就可以看到 `getUserProfile` 方法的回傳值。 #### HTTP 狀態碼 (HTTP Status Code) 預設情況下,除了 POST 請求會回傳 201 狀態碼,其他 HTTP 方法的狀態碼都會是 200。不過應該要以實際情況來回傳適當的狀態碼。 Nest 提供了狀態碼的 enum,並用 `@HttpCode()` 裝飾器來設置狀態碼。這裡我們將 `getUserProfile` 方法的狀態碼設置為 204: ```typescript @Controller("users") export class UsersController { @Get("profile") @HttpCode(HttpStatus.NO_CONTENT) getUserProfile() {} } ``` #### 主體資料 (Body) 在傳輸資料時,有時候會需要傳遞主體資料,Nest 提供了 `@Body()` 裝飾器來取得主體資料。這裡我們做一個簡單的範例: ```typescript @Controller("users") export class UsersController { @Post("profile") getUserProfile(@Body() body: { name: string; age: number }) { const { name, age } = body; return { name, age }; } } ``` 這時使用 Postman 發送 POST 請求到 <http://localhost:3000/users/profile>,並在 Body 中填入 `name` 和 `age`,就可以看到 `getUserProfile` 方法的回傳值。 #### 資料傳輸物件 (DTO) 什麼是 DTO?他的全名是 Data Transfer Object,中文翻譯為資料傳輸物件,通常用於過濾、格式化資料,避免在傳輸過程中發生錯誤。他只負責存放要傳遞的資訊,故只有唯讀屬性,沒有任何方法。定義 DTO 後,就不必一直翻文件查詢資料的格式,可以很清楚的知道要傳遞的資料格式。在 Nest 中,甚至可以基於 DTO 來進行驗證,進而大幅降低維護成本。 既然是定義格式,那麼就有兩種選擇: - 使用類別 (Class) - 使用介面 (Interface) 基本上會建議使用類別的形式來建立 DTO ,原因是介面在編譯後會消失,而類別會保留,這對部分功能是有影響的,所以**官方也建議使用類別**。 這裡我們先做個簡單的範例,在要調整的控制器目錄中建立一個 `dto` 資料夾,並在裡面建立一個 `create-user.dto.ts` 檔案: ```typescript export class CreateUserDto { public readonly name: string; public readonly age: number; } ``` 建立好 DTO 後,就可以在控制器中使用,這裡我們將 `getUserProfile` 方法的參數改為使用 DTO: ```typescript @Controller("users") export class UsersController { @Post("profile") getUserProfile(@Body() body: CreateUserDto) { const { name, age } = body; return { name, age }; } } ``` #### 標頭 (Headers) 有時候在傳輸資料時,會需要傳遞標頭,例如 `Authorization` 或 `Content-Type` 等,Nest 提供了 `@Headers()` 裝飾器來取得標頭。這裡我們做個簡單的範例: ```typescript @Controller("users") export class UsersController { @Get() @Header("Cache-Control", "none") getUsers() { return "users"; } } ``` #### 重定向 (Redirect) Nest 提供了 `@Redirect()` 裝飾器來進行重定向,用來將請求導向到另一個 URL。常見用途包含將舊的 URL 導向到新的 URL、登入成功後導向到首頁、權限不足時導向到登入頁面,以及將 HTTP 導向到 HTTPS。這裡我們做個簡單的範例: ```typescript @Controller("users") export class UsersController { @Get() @Redirect("https://www.google.com", 301) getUsers() {} } ``` #### 參數裝飾器 (Parameter Decorators) 前面有提過 Nest 是以 Express 或 Fastify 作為底層基礎進行整合的框架,在很多地方都是對底層平台進行包裝的,其中的參數正是包裝出來的,透過特定的參數裝飾器來取得不同的資訊,除了前面提及的幾項以外,還有提供許多參數裝飾器來提供開發人員取得更多資訊: - `@Req()`:求的裝飾器,帶有此裝飾器的參數會賦予底層框架的 請求物件 (Request Object)。 - `@Res()`:回應的裝飾器,帶有此裝飾器的參數會賦予底層框架的 回應物件 (Response Object)。 - `@Next()`:Next 函式的裝飾器,帶有此裝飾器的參數會賦予底層框架的 Next 函式,用途為呼叫下一個 中介軟體 (Middleware)。 - `@Session()`:用於取得 Express 的 Session 物件。 - `@Param()`:用於取得路由參數。 - `@Query()`:用於取得查詢參數。 - `@Body()`:用於取得主體資料。 - `@Headers()`:用於取得標頭。 - `@Ip()`:用於取得請求的 IP 位址。 - `@HostParam()`:用於取得主機參數。 #### 處理回應的方式 前面使用的範例都是透過 `return` 的方式處理回應,這是 Nest 預設的處理方式,稱為 **標準方式**。事實上,Nest 還提供了 **函式庫方式** 來處理回應,使用 `@Res()` 裝飾器。 - 標準方式 透過 `return` 的方式處理回應,是 Nest 預設的處理方式,稱為 **標準方式**。 ```typescript @Controller("users") export class UsersController { @Get() getUsers() { return "users"; } } ``` - 函式庫方式 透過注入特定框架的響應物件,使用 `@Res()` 裝飾器。 ```typescript @Controller("users") export class UsersController { @Get() getUsers(@Res() res: Response) { res.send("users"); } } ``` #### 模式的限制 Nest 會去偵測是否有帶 `@Res`、`@Response`、`@Next` 裝飾器的參數,如果有的話,該資源就會啟用函式庫模式,而標準模式會被關閉,這是什麼意思呢?簡單來說 `return` 值的方式會失去作用。下方為範例程式碼: ```typescript @Controller("users") export class UsersController { @Get() getUsers(@Res() res: Response) { res.send("users"); } } ``` 可以發現 `getUsers` 方法的 `return` 值失去了作用,這是因為 `@Res()` 裝飾器啟用了函式庫模式,導致 `return` 值的方式失效。 那如果想要同時使用標準模式和函式庫模式呢?可以使用 `@Res({ passthrough: true })` 來實現。 ```typescript @Controller("users") export class UsersController { @Get() getUsers(@Res({ passthrough: true }) res: Response) { res.send("users"); } } ``` ## 服務 (Service) ## Reference [NestJS 官方文件](https://docs.nestjs.com/) [NestJS 中文網](https://nest.nodejs.cn) [NestJS 帶你飛!](https://ithelp.ithome.com.tw/users/20119338/ironman/3880)