Controller

路由

路由名稱會透過 @Controller 傳入元數據 (metadata),在應用程式啟動時建立一個路由表並將這個元數據註冊進去,如:

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

'todos' 就會被註冊成可以存取的端點,可以透過 http://localhost/todos 這樣的網址發送請求。

資訊

@Controller 也會被註冊到 IoC Container 裡面。


請求

必須使用 HTTP method 裝飾器,才會將對應方法 (handler) 註冊到路由表,如:

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

此時對 /todos 發送 GET 請求,執行 getTodos 成功的話會並得到 []


子路由

@Get 裡面帶入字串會生成子路由的端點,如 @Get('sub')則表示可以存取 /todos/sub

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

  @Get('sub')
  getTodo() {
    return '這是子路由';
  }
}

通用路由

可以透過 *+? 等符號來匹配有滿足特定條件的路徑,如:

// 這樣可以匹配 todos/bulk/goooooooood
@Get('bulk/goo*d')
getGood() {
  return '這是 /bulk 下面的通用路由 goo*d';
}

裝飾器

handler 的參數可以用裝飾器來解析資料,如:

  • @Param
  • @Query
  • @Body
  • @Header

@Param

動態路由可以透過 @Param 來解出路由中的參數,例如下面的 :id

// 解析 /todos/:id
@Get(':id')
getTodo(@Param() param: { id: string }) {
  return `這是 id 為 ${param.id} 的子路由`;
}

// 更簡短的寫法,在裝飾器中指定 key 就不用取出整個物件
@Get(':id')
getTodo(@Param('id') id: string) {
  return `這是 id 為 ${id} 的子路由`;
}

@Body

@Post()
createTodo(@Body() data: { content: string }) {
  const newTodo = {
    id: this.todos.length + 1,
    content: data.content,
  };

  this.todos.push(newTodo);

  return newTodo;
}

@Query

// 解析 /todos?limit=3&offset=3
@Get()
getTodos(
  @Query('limit') limit?: string,
  @Query('offset') offset?: string
) {
  if (!limit) {
    return this.todos;
  }

  if (!offset) {
    offset = '0';
  }

  const limitNum = parseInt(limit);
  const offsetNum = parseInt(offset);

  return this.todos.slice(offsetNum, offsetNum + limitNum);
}

@HttpCode

除了 POST 請求會回應 201 之外,其他方法預設都會回應 200,如果要自訂回傳的狀態碼,可以使用 @HttpCode 裝飾器,並帶入內建的常數 HttpStatus

// 請求成功時使用 NO_CONTENT 映射出來的 204 作為狀態碼
@Get()
@HttpCode(HttpStatus.NO_CONTENT)
getTodos() {
  return [];
}

HttpStatus 裡面是用 enum 型別宣告的映射資料,包含了常見的狀態碼與語意匹配。


回應

controller 有 3 種方式處理回應:

  1. 標準模式
  2. RxJS 模式
  3. 函式庫模式

標準模式

標準模式支援同步和非同步,也是官方推薦的方式:

@Get()
getData() {
  return [];
}

// 被 setTimeout 延遲,會晚一點收到回應
@Get()
async getAsyncData() {
  return new Promise((resolve) => {
    setTimeout(() => resolve([]), 1000);
  })
}

RxJS 模式

回傳一個 Observable 物件 of,NestJS 會訂閱這個物件的狀態,of 後面可以鏈式串上各種 RxJS 組織資料的方法,整個鏈式的任務結束後會將最後形成的資料送出。

資訊

對呼叫它的前端或是其他服務的請求來說,一樣會收到一個非同步的回應,RxJS 的任務只存在於應用程式內部。

import { catchError, map, of } from 'rxjs';

// 使用 RxJS 的鏈式方法重新組織資料
@Get('data/rxjs')
getRxjsData() {
  return of(this.todos).pipe(
    map((todos) =>
      todos.map((todo) => ({
        ...todo,
        status: 'active',
      })),
    ),
    catchError((err) => {
      console.error('Error occurred:', err);
      return of([]);
    }),
  );
}

函式庫模式

可以從底層的 API 來控制回應內容,需要在 handler 裡面加入裝飾器標記,如 @Request@Response@Next,對應到 Express 的 reqresnext,加上標記後就如使用 Express 一樣產生回應:

@Get('data/lib')
getLibraryData(@Res() res: Response) {
  res.status(200).send('這是從 library 來的資料');
}

通常會在:

  1. 串流任務 (streaming)
  2. 某些套件只支援 Express 的回應物件
  3. 完全控制回應程序的設定與資料

才會使用到函式庫模式。因為會繞過 NestJS 的設定與元件流程,所以回應內容與格式要自行重新調整。


小結

controller 的功能與一般 MVC 架構雷同,只是大部分的操作都需要用裝飾器取代,需要花點時間轉換,但只要記得 裝飾器是一種函式,給出對應的參數就能得到相應的操作,減少反覆宣告、賦值等等的程式碼。


參考資料