tags: NestJS

NestJS

Introduction

Nest是一款在Node.js環境下的框架用來打造具有效率且具擴充性的伺服器端應用程式,他支援了完整的TypeScript撰寫環境(同時也支援單純JavaScript撰寫),並且結合了OOP, FP, FRP(Funtional Reactive Programming)的元素。
Nest的底層是採用了Express這個強大的伺服器框架,但也可以有選擇設定成Fastify的選項。

Philosophy

近年來JavaScript, Node.js在前後端成為了廣泛使用的語言,有許多新興的套件框架崛起例如,Angular, React 和Vue,但雖然有許多的好的套件、工具等等,但其中都沒有解決的一個問題就是架構(Architecture)。

Nest提供了開發者即用的程式架構,讓開發者們可以建立高度可擴展、測試、低耦合且易維護的程式,這個架構高度受到Augular啟發。

Installation

以下使用 Nest CLI的方式安裝,並且建立新的專案。
註: 需要先在電腦上安裝node環境,且確認npm可以使用

$ npm i -g @nestjs/cli
$ nest new project-name

會建立一個新的project路徑,並建立Nest核心的檔案跟modules,會建造一個常規結構。

Overview

First steps

接下來你會學習到Nest的核心基礎core fundamentals,為熟悉Nest內基礎且必須的區塊,下面會建立一個簡單的CRUD app,並涵蓋許多其基本的功能。

Language

下面介紹皆會使用TypeScript作為主要語言。

Prerequisites

請確保安裝Node.js,且版本在12以上(但不包含13)。

Setup

建立新專案

$ npm i -g @nestjs/cli
$ nest new project-name

HINT
To create a new project with TypeScript's strict mode enabled, pass the strict flag to the nest new command.

下完指令後,project-name 會被建立,node_modules跟其他樣板檔案也會被建立,以及一個src/資料夾也會被建立且產生其中幾支核心檔案。
以下簡介:

name brief
app.controller.ts A basic controller with a single route.
app.controller.spec.ts The unit tests for the controller.
app.module.ts The root module of the application.
app.service.ts A basic service with a single method.
main.ts The entry file of the application which uses the core function NestFactory to create a Nest application instance.

main.ts包含了一個async function可以用來引導我們的app。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

為了建立一個Nest的實例,我們使用NestFactory這個核心class,NestFactory提供了些靜態method來建立app實例。 create()method會回傳一個app物件,滿足了INestApplication這個介面(Interface),這個物件提供了一些method,後面會再提到,上面的例子,我們簡單地開啟一個HTTP listener, 讓app等待後續進來的HTTP request。

以上藉由Nest CLI建立的資料夾提供了基礎的文件結構,而後續也鼓勵開發者按照此結構將不同的module放在各自的module資料夾內。

Platform

Nest目標為一個與平台無關的框架,平台獨立性讓其可以建立任何可以重複使用的邏輯,讓開發者可以藉由此讓其運行在許多不同的app上,技術上來說Nest可以與任何Node Http框架相互配合,有兩個內建支援的框架:Expressfastity。你可以選擇你想要的,Express為預設基礎,想使用Fastify可看這裡
不管使用哪個platform,都有其介面,分別為NestExpressApplication NestFastifyApplication

當你在NestFactory.create()後傳入一個型別,這app物件會擁有你輸入的那個platform的method,請注意,你不需要真正傳一個型別除非你真的想要存取那個platform下的API。如下範例:

const app = await NestFactory.create<NestExpressApplication>(AppModule);

Running the application

npm run start

這個指令會讓app啟動並且監聽你在sec/main.ts指定的port。

npm run start:dev

這個指令可以讓你開啟伺服器,並且持續監聽變化、編譯且自動重啟server。

Controllers

Controller的責任就是接收client端傳來的request並且回傳response。
Controller的目的就是用來幫app接收特定的requrest,路由機制會控制哪個controller接收哪個request,通常情況下,一個controller可能被多個路由使用,而不同的路由可以有不同的動作。

為了建立基礎的controller,我們使用classesdecoratorsDecoratorsclasses協同需求資料並讓Nest建立一個rounting map(綁定此request與某一個相對的controller)

HINT
For quickly creating a CRUD controller with the validation built-in, you may use the CLI's CRUD generator: nest g resource [name].

Routing

以下的範例使用@Controller() decorator,這要引用一個基礎的basic controller,我們使用一個path prefix(路徑前綴)為cats,使用path prefix幫助我們關聯所有與其相關的路由,並將重複的code減到最少。

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

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

你也可使用CLI的方式來建立一個controllernest g controller cats

這個@Get() decorator告訴Nest要建立一個處理器針對這個特定req。簡單來說就是會處理 GET / cats的req,或是GET /cats/abc

請注意在@Get()後有一個 findAll()method,這是Nest為了示範而隨意取名的,我們確實需要需要用一個method名稱接續在後面,但Nest不會指定任何特定的method名稱。

這個method會回傳status code 200並帶上一個字串,下面介紹Nest內提供的兩個res方式:

  1. Standard(推薦)
    內建的方式為,當要回傳一個object或是array的話,Nest自動將其序列化成JSON格式,如果是回傳primitive type(e.g string, number, boolean)Nest就會直接回傳並不將其序列化。 這讓res處理簡單許多,只要管回傳就好,Nest會幫你做好其他的事。
    此外回傳的預設status code會是200,除了像是POST會是201,而要改變的話,就要引用另外一個decorator@HttpCode(...) at a handler level。

  2. Library-specific
    如果使用此方法,要使用@Res()decorator,在method handler signature的那段(e.g findAll(@Res() response)),這樣的話,你就可以使用物件的方式回傳res.像是response.status(200).send()

WARNING
Nest detects when the handler is using either @Res() or @Next(), indicating you have chosen the library-specific option. If both approaches are used at the same time, the Standard approach is automatically disabled for this single route and will no longer work as expected. To use both approaches at the same time (for example, by injecting the response object to only set cookies/headers but still leave the rest to the framework), you must set the passthrough option to true in the @Res({ passthrough: true }) decorator.

Request Object

handler通常要存取client的req細節,Nest提供存取req object的途徑是以底層的方式(預設為Express)我們可以加上@Req()在handler signature。下面範例:

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

為使用expresstyping可以安裝套件 npm i @types/express

這request object代表了HTTP request,在多數情況下,你不需要手動取得那些屬性,可以使用
設定好的decorator來替代,例如這邊說的列表。

為了更好地兼容底層的platform,Nest提供了@Res()@Response()@Res()只是@Response的替代名稱,請注意當你使用@Res()@Response()時,Nest會轉為Library-specific模式,所以你要特別注意response object(e.g., res.json(...) or res.send(...)),的回傳,否則你的伺服器會停在那邊沒有反應。

Resources

現在換來看POSThandler

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

就像範例所示,相當簡單,Nest提供所有標準的HTTP method: @Get(),@Post(),@Put(),@Delete(),@Patch(),@Option(),@Head(),@All().

Route wildcards

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

ab*cd這個路由會符合abcd,ab_cd,abecd以此類推其餘符號?,+,*,()也可以被使用在路由中。

Status code

如前所提到,預設回傳的status code都是200除了POST request是201,我們可以輕鬆的更改status code藉由加入@HttpCode(...)這個decorator at a handler level.

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

HINT
Import HttpCode from the @nestjs/common package.

通常的情況下statu code不是靜態的,而是取決於各種因素。而這種情況下,可以使用library-specific response(使用@Res() 注入)對象(或者,如果出現錯誤,則拋出錯誤)

Headers

為表達特定的header,你可以使用@Header()注入或是使用library-specific response,直接呼叫res.header()

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

Redirection

為redirect到一個特定的URL,你可以使用@Redirect()注入或是使用library-specific response,直接呼叫res.redirect()
@Redirect()接受兩個參數,分別為urlstatusCode,兩個皆為optional,但預設code為302(Found)。

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

有時候你可能會想根據不同狀況redirect到不同url或是有不同statuCode,可以return下面的物件來替換:

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

回傳值會覆蓋掉任何目前在@Redirect()裡面的參數,例如:

@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
  }
}

Route parameter

固定路由無法滿足對於某些動態查詢例如(GET /cats/1 拿到id為1的cat) ,為了藉由這些參數來定義這些路由,我們可以加上參數token來滿足這些動態需求。路由參數會放在@Get()注入器內,而@Param()注入器應該要放在method signature裡面。以下範例:

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

@Param()是被用來在method的注入器,並讓路由參數可以像屬性一樣被調用,上面的範例,我們就可以調用id藉由params.id,你也可以傳入一個特定的參數token到注入器內,必且後續在method body直接使用。如下:

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

Sub-domain Routing

The @Controller decorator can take a host option to require that the HTTP host of the incoming requests matches some specific value.

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

WARNING
Since Fastify lacks support for nested routers, when using sub-domain routing, the (default) Express adapter should be used instead.
Similar to a route path, the hosts option can use tokens to capture the dynamic value at that position in the host name. The host parameter token in the @Controller() decorator example below demonstrates this usage. Host parameters declared in this way can be accessed using the @HostParam() decorator, which should be added to the method signature.

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

Scopes

For people coming from different programming language backgrounds, it might be unexpected to learn that in Nest, almost everything is shared across incoming requests. We have a connection pool to the database, singleton services with global state, etc. Remember that Node.js doesn't follow the request/response Multi-Threaded Stateless Model in which every request is processed by a separate thread. Hence, using singleton instances is fully safe for our applications.

However, there are edge-cases when request-based lifetime of the controller may be the desired behavior, for instance per-request caching in GraphQL applications, request tracking or multi-tenancy. Learn how to control scopes here.

Asynchronicity

Nest支援完整的async/awaitfunction。
每個asyncfunction都會回傳一個Promise,這表示你可以回傳一個延遲的值回來,Nest會將其resolve,以下範例:

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

Request payloads

前面的POST範例中,並沒有加入任何client params,我們加入注入器@Body()來修復這問題。
首先,如果你使用的是TS,你要先決定DTO(data transfer object)的schema,我們可以用classes或是interface來定義這個schema,但Nest推薦的是使用classes,因為JS就包含classes的支援,經過編譯後還是可以保留所有的樣貌,這很重要,因為後面有一項功能Pipes會需要這樣的使用方式。
以下先建立一個 CreateCatDto 的class:

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

然後將其使用在controller裡面

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

Library-specific approach

前面有探討到關於操作response的方式有Nest standard way以及Library-specific,後者需要注入器@Res(),以下範例來展現不同之處:

**Nest standard way**
@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)`;
  }

**Library-specific approach**
@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([]);
  }
}

後者方式確實讓回傳的方式更有彈性,但要注意的是,這種方法可能會更不清楚以及會有下壞處,最主要的壞處為你的code就變成platform-dependent以及不好去做測試(你還要mock這個response物件),另外你也是去了跟Nest相容的功能,像是@HttpCode()/@Header()注入器,為了改善這個你可以傳入passthrough option並設為true。如下範例:

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

Now you can interact with the native response object (for example, set cookies or headers depending on certain conditions), but leave the rest to the framework.


Providers

Providers在Nest裡是個基本概念,許多在Nest裡基礎的classes會被當作是provider - services, repositories, factories, helpers,Provider最主要的概念為他可以被當成一個dependency注入,代表這物件可以在不同地方建立起連結,而這種連結的實例可以很大程度地委託給Nest runtime system。
Controller應該處理HTTP request並把更複雜的任務留給providers,Provide是純JS classes,在module裡面被稱作providers。

Services

來建立一個CatsService,這個service會負責資料的存儲以及檢索,並且是給CatsController使用,所以是個被定義成provider的候選者。

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

這個CatsService是相當簡易的class只有一個property以及兩個method,只有一個新功能就是他使用了@Injectable這個注入器,這注入器連結了metadata,說明了CatsService可以被Nest IoC container所管理。另外這範例也使用了Cat interface,像如下:

export interface Cat {
  name: string;
  age: number;
  breed: string;
}

現在有了CatsService,就來引入CatsController

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

CatsService透過class contructor的方式被注入,請注意這裡使用到了private的語法,這裡的方法允許我們在同一個位置同時宣告並且初始化CatsService

Dependency injection

Nest是依照依賴注入的設計模式而打造成。可以參考Angular對於DI的解釋

在Nest中,感謝TS的能力,使其非常容易來管理這些dependencies藉由對於type的控制。

Scope

Providers normally have a lifetime ("scope") synchronized with the application lifecycle. When the application is bootstrapped, every dependency must be resolved, and therefore every provider has to be instantiated. Similarly, when the application shuts down, each provider will be destroyed. However, there are ways to make your provider lifetime request-scoped as well. You can read more about these techniques here.

Custom providers

Nest has a built-in inversion of control ("IoC") container that resolves relationships between providers. This feature underlies the dependency injection feature described above, but is in fact far more powerful than what we've described so far. There are several ways to define a provider: you can use plain values, classes, and either asynchronous or synchronous factories. More examples are provided here.

Optional providers

如果provider不一定會被resolve,使用@decorator注入器標注為選擇性的。

Provider registration

前面已經定義了一個provdier(CatsService),且我們也有了一了comsumer(CatsController),接下來我們必須將其註冊,這樣Nest才會知道他可以被使用,可以藉由編輯module file(app.module.ts)把@module 注入器的provider array加上新的provider。

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

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

Manual instantiation

Thus far, we've discussed how Nest automatically handles most of the details of resolving dependencies. In certain circumstances, you may need to step outside of the built-in Dependency Injection system and manually retrieve or instantiate providers. We briefly discuss two such topics below.

To get existing instances, or instantiate providers dynamically, you can use Module reference.

To get providers within the bootstrap() function (for example for standalone applications without controllers, or to utilize a configuration service during bootstrapping) see Standalone applications.

Modules

一Module是由@Module()注入器註解過的class,此@Module()注入器提供metadata來讓Nest利用其規劃整個程式結構。

每個app至少都有一個module(a root module),root module是Nest用來建立application graph的起始點。Application graph - Nest的內部資料結構以用來resolve module和provider的關係和依存。雖然有些小程式可能就只有一個module(root module),但這裡想強調的是Nest強烈推薦使用module作為一個有效管理component的方式。因此,對大多數app來說,最終的結構會是使用多個modules,且各個都有相當的依存性。

@Module()注入器為一個簡單的物件方式來描述:

properties describe
providers the providers that will be instantiated by the Nest injector and that may be shared at least across this module
controllers the set of controllers defined in this module which have to be instantiated
imports the list of imported modules that export the providers which are required in this module
exports the subset of providers that are provided by this module and should be available in other modules which import this module. You can use either the provider itself or just its token (provide value)

module預設會將provider做封裝,這代表不可能將provider注入當前部署於其一部份的module也不能將從引入的module中匯出。因此,你可以將一個module裡面的provider當作為一個module's public interface or API.

Feature modules

CatsControllerCatsService屬於同一app domain,因為他們關係如此接近,所以將其分出來放到同一個feature module也是合理的,Feature module用以簡單組織有相關特定功能的code,並且使其簡潔且建立明確的權責界線。為了展示,以下為CatsModule:

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

可以藉由CLI指定來建立module. nest g module cats

上面我們將跟CatsModule有關的都封裝進去了,接下來要在主要Appmodule將其引入如下:

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

Shared modules

在Nest裡,module為單獨存在的,因此你可以在各不同的module之間分享並使用任何provider instance。
任何module皆為是shaerd module,當只要一被建立,就可以被任何module所引用,以下範例為想將CatsService變成個module都可以使用惡instance。我們首先要將其export,如下:

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

現在任何module只要引用CatsModule都可以對CatsService進行存取。

Module re-exporting

如下範例:module可以將import的module,再次export。

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

Dependency injection

A module class can inject providers as well (e.g., for configuration purposes):

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private catsService: CatsService) {}
}

However, module classes themselves cannot be injected as providers due to circular dependency .

Global modules

如果你需要在許多地方引入同一組module,這會變得相對麻煩,不同於Nest,Angular將provider註冊在global scope,可以在任何地方被存取,而Nest是將其封裝在module scope,如果沒有先將其引入你是無法使用的。
當你想要在任何地方都使用某些module時(e.g helpers, database connection),你可以使用@Global注入器讓其變成global scrope,如下:

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

這樣CatsService就可以在任何地方被存取,且無需要再引入CatsModule在其import陣列內。

讓所有module都為global並不是個好設計模式,他只是用來減少某些必要的步驟,整體而言,import array 還是為推薦的方式。

Dynamic modules

???

Middleware

Middleware是一個route handler前面被呼叫的function。Middleware可以存取request 和 response 物件,以及在req-res循環中的next() middleware function。

Nest middleware,預設上,就和express middleware相同,下面為express官方文件對於middleware的作為:

Middleware可以執行下列任務:

  • execute any code

  • make changes to the request and the response objects.

  • end the request-response cycle.

  • call the next middleware function in the stack.

  • if the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.

你使用客製化的Nest middleware,不是一個function就是一個class並搭配@Injectable注入器,這class應搭配NestMiddlewareinterface來實現。但如果是function方式的話將無此要求,下面先看使用class方式的middleware:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}

Dependency injection

Nest middleware也完整支援dependency injection,就像providers和controllers一樣,他們可以被注入到同個module底下,藉由constructor就像其他方式一樣。

Applying middleware

Middleware並不是在@Module注入器內使用,而是用module底下的configure()方式,包含middleware的module都必須要間接NestModule的interface。如下:

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

我們可以進一步限制此middleware被使用的地方,藉由傳入一組物件裡面包涵pathmethodforRoutes()這個方法內部,如下:

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}

Middleware comsumer

MiddlewareConsumer是個helper class,它提供了許多內建的方法來管理middleware,他們都可以被簡單的串連在一起,forRoutes()這個方法可以接收單一個string或是多個string,或是一個RouteInfo物件,或一個甚至多個controller class,在多數情況下可能只會用到一連串的controllers由,做分離。

Excluding routes

有時我們有可能會想剔除一些不想要的routes去使用的middleware,我們可以用exclude(),這方法可以接收一個string或多個string或是一個RouteInfo物件來表達出想要被排除的route,如下所示:

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/(.*)',
  )
  .forRoutes(CatsController);

Functional middleware

我們還可以使用function的方式來寫一個middleware,這類的middleware就叫做Functional middleware。

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
};

當middleware無需任何dependency注入時,盡量使用簡單的functional middleware。

Multiple middleware

為了綁定不同的middleware並讓其依序執行,簡單使用,來進行middleware之間的分離:

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

Global middleware

當我們想要將middleware使用在任何route上面時,可以使用use(),由NestApplication實例所提供:

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

HINT
Accessing the DI container in a global middleware is not possible. You can use a functional middleware instead when using app.use(). Alternatively, you can use a class middleware and consume it with .forRoutes('*') within the AppModule (or any other module).

Exception filters

Nest有內建的錯誤處理層,用來處理所有沒有被處理到的錯誤,當有個錯誤沒有被你的codebase處理到時,它就會被這層捕捉住,並且送出易讀的回應。

這個行為就是屬於內建的global exception filter所執行,用以處理HttpException類別的錯誤,當有一錯誤無法被辨認時,他就會是屬於HttpExceoption或是繼承其類別的,內建的exception filter會產生以下預設的JSON response.

{
  "statusCode": 500,
  "message": "Internal server error"
}

Throwing standard exceptions

Nest提供內建的HttpException類別,從@nestjs/commonpackage,典型的HTTP REST/GraphQL類型的app,當有錯誤發生時,實務上做好的做法就是回傳標準的HTTP response物件,以下舉例,在CatsController內,有個findAll()方法,假設它因為某原因會丟出一個錯誤(exception):

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

範例內使用HttpStatus這是help enum從@nestjs/commonpackage引入的。

當用戶端收到錯誤訊息時,看起來會像下面:

{
  "statusCode": 403,
  "message": "Forbidden"
}

HttpException建構子會需要兩個必填的參數用以決定回應格式:

  • response參數定義的是JSON response body,可以是個stringobject
  • status參數定義的是HTTP status code。

預設中,JSON response body包含兩個properties

  • statusCode: 默認為HTTP狀態碼由參數status所提供。
  • message: 一段根據HTTPstatus的簡短描述

為改寫部分JSON response body的message, 提供一段字串在response參數內,為改寫整個JSON的reponse body,傳入一個物件到response參數內,Nest會將其物件序列化(serialize)並且將其當作JSON response body回傳。
第二個參數-status,應為一個有效的HTTP status code,最好的實作方式為引用@nestjs/common內的HttpStatusenum。以下範例:

@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}

使用以上的response會是如下:

{
  "status": 403,
  "error": "This is a custom message"
}

Custom exceptions

在多數時候,你不太需要寫客製的exceptions,如果真的有需要建立自己客製的exceptions,建立自己的exception hierarchy(等級)會是比較好的做法,由base HttpException所繼承出來的客製化exception,藉由此方式,Nest可以認出你的exception,並自動處理錯誤回應,下面custom exception範例:

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

因為ForbiddenException是繼承自HttpException,他與內建的錯誤處理器無太大差別,因為可以將其使用在findAll()方法內:

@Get()
async findAll() {
  throw new ForbiddenException();
}

Built-in HTTP exceptions

Nest提供了一組標準的錯誤處理以HttpException為基礎,這些都引用自@nestjs/common,並可以代表大部分常見的HTTP exceptions:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

Exception filters

雖然內建的exception filter可以自動處理許多狀況,但你可能會想完全掌握exception layer。例如你可能會想加入log或是使用不同的JSON schema根據不同的變動因素,Exception filters 就是為此目的而被設計的,它讓你完整控制流程以及回傳給client的response。
下面建立一個用來接住繼承自HttpException的錯誤實例,並且添加客製的回傳response,為此我們要存取底層的RequestResponse物件,藉由存取Resquest可以拿出originalurl並把他加入log內,並利用Response物件來直接回傳response,使用response.json()方式。

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

所有的exception filter都要使用generic泛型 ExceptionFilte<T>這要求你要提供catch(exception: T, host: ArgumentsHost)方法並帶有其指定的簽名(indicated signature),T指的就是exception的type

@Catch(HttpException)注入器將需要的metadata綁定到exception filter,特別告知了Nest要特別尋找屬於HttpException類型的錯誤其餘都不要,這個@Catch()注入器可以接受一個或是多個由逗點分格的參數,可以讓你一次設定許多不同的類型的錯誤。

Arguments host

讓我們來看catch()方法的參數,exception參數就是目前正在被處理的錯誤物件,host參數就是一個ArgumentHost物件,ArgumentHosst是個強力的工具物件,後續會在 execution context chapter多做詳解,在上面的code範例中,我們用其來取得被傳到原始request handler中ResquestResponse物件的參照,在code範例中,我們在ArgumentHost上使用了一些helper method來取得想要的RequestResponse物件。

Binding filters

讓我們將新的HttpExceptionFilterCatsControllercreate()做綁定。

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

@UseFilters()注入器是從@nestjs/common引入。

@UseFilters()注入器就如同前面說的@Catch()一樣,可以接受一個或是多個由逗點隔開的參數,這裡我們建立了一個HttpExceptionFilter實例,相對的你可以將class傳入並將實例化的職責以及啟動dependency injection的工作留給framework。

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

建議使用下面這個傳入class的方式,可以減少記憶體使用因為Nest可以輕鬆地在整個module內重複使用同一個class的實例。

上面的例子中,HttpExceptionFilter只有被使用在單一個create()route handler,讓其為method-scoped。Exception filters可以是: method-scoped, controller-scoped或是global-scoped. 例如為建立一個controller-scoped的filter你可以做以下:

@UseFilters(new HttpExceptionFilter())
export class CatsController {}

這個設定讓CatsController內的每個route都有HttpExceptionFilter
為建立global-scoped的filter,你可以做以下:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

The useGlobalFilters() method does not set up filters for gateways or hybrid applications.

Global-scoped是整個application都會用到的,每個controller每個route handler都會用到,按照dependency injection,global filter會註冊在任何module的外層(使用 useGolbalFilters())無法注入dependencies因為這是在任何module的context的外層。為解決此問題,你可以在任何module直接註冊一個global-scoped filter,使用以下範例方式:

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

Catch everything

為接住所有沒有被處理到的錯誤(無論任何錯誤type),讓@Catch()注入器的的參數留空 e.g, @Catch()

Pipes

A pipe is a class annotated with the @Injectable() decorator, which implements the PipeTransform interface.

Pipes有兩種經典的用法:

  • transformation(轉換):將input的data轉換成想要個格式(e.g, 從string轉換成number)
  • validatoin(驗證):檢驗input data是否為valid,valid就通過,invalid就throw exception.

兩種情況下,pipes會透過由controller route handler處理的參數進行操作,Nest會在method被呼叫前插入(interposes)一個pipe,而這pipe就會接收到這個method的參數並對其進行處理。任何轉換或驗證都會在這階段被執行,在這之後route hander才會對被pipe處理過的參數進行動作。

Nest有幾個內建開箱即用的pipe,你也可以建立客製化的pipe,在此章節將會介紹一些內建的pipes並展示如何將其與route handler 進行綁定,隨後也會介紹客製化的pipes也會告訴你如何從最根本建立客製化pipe。

Pipes run inside the exceptions zone. This means that when a Pipe throws an exception it is handled by the exceptions layer (global exceptions filter and any exceptions filters that are applied to the current context). Given the above, it should be clear that when an exception is thrown in a Pipe, no controller method is subsequently executed. This gives you a best-practice technique for validating data coming into the application from external sources at the system boundary.

Built-in pipes

Nest有幾個內建開箱即用的pipes:

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe
    他們全都引用自@nestjs/common

先快速看一下ParseIntPipe,這是個轉換(transformation)的範例,透過此pipe會確保進到method handler的參數會是一個JS integer(或是拋出錯誤如果無法轉換的話)

Binding pipes

為使用pipe,我們需要將pipe class的實例綁定到適當的位置,在ParseIntPipe例子中,我們想要將其與特定的route handler進行合作,並確保此pipe會在method被呼叫前就先進行,以下的例子就是將pipe綁定在method參數:

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

這能確保以下兩種情況為true: findOne()接收到的參數將會是一個整數數字,或是在route handler被呼叫前就拋出錯誤。

假如下面的route被呼叫:
GET localhost:3000/abc

Nest會丟出像下面的錯誤:

{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

這個錯誤處理可以避免findOne() method被執行到。

上面的例子中我們傳入了的是ParseIntPipe的class,將實例化的責任留給了framework以及啟用dependency injection。Pipes和guards和都一樣,我們可以傳入一個實例,使用時力的方式也有助於我們客製化內建pipes的行為藉由傳入options的方式:

@Get(':id')
async findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {
  return this.catsService.findOne(id);
}

綁定其他的轉換pipes也是差不多的方式,這些pipes都可以使用在parameters, query string和body values。

以下例子為一個queyr string parameter:

@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

以下為使用ParseUUIDPipe的範例,用來檢驗字串是否為一UUID。

@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
  return this.catsService.findOne(uuid);
}

當使用ParseUUIDPipe()你要驗證不同版本的UUID時,你可以藉由option傳入一個版本數字來驗證。

上面看了一些內建的Parse* family的綁定方式,綁定驗證(validation)pipe會有點不同,後續會在討論。

你也可以看這這篇講驗證技巧的文章裡面有幾個廣義的關於validation pipe的範例。

Custom pipes

如前面所說,雖然Nest提供了強大的內建ParseIntPipeValidationPipe功能,你還是可以建立你自己的客製化pipe,以下會從頭建立一個簡易的客製化pipe以展示如何建立。 從一個簡單的ValidationPipe開始,一開始我們會模擬一個function接收一個input value並隨即回傳此value。

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

PipeTransform<T, R>是任何pipe都要使用的泛型介面(generic interface),T代表的是input value的type,R指的是transform()的回傳型別。

每個pipe都一定要執行transform() method方法來滿足PipeTransform介面規則,這method有兩個參數:

  • value
  • metadata

value參數是目前被處理的參數(在route handler接收到之前的),metadata參數是目前被處理的參數metadata,這metadata物件有以下的屬性:

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

這些properties描述了以下正在被處理的參數

type 表示此參數是否為@Body(),@Query(),@Param()或是客製化的parameter
metatype 提供參數的元類型,例如String,注意:如果你忽略宣告型別或是使用原生JS撰寫此value會是undefined
data 傳到注入器的string,例如:@Body('string'),如果未傳入值到注入器內則此值將為undefined

TS的interface會在轉譯過程中消失,因此如果有一參數型別為interface宣告的話而非class宣告,metatypevalue會是一個Object

Schema based validation

Object schema validation

Binding validation pipes