---
# System prepended metadata

title: 控制器揭秘：掌控 HTTP 請求的藝術
tags: [NestJS]

---

控制器揭秘：掌控 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)
