はじめに
前回( 【NestJS】ガチ入門【その1:NestJSの概要とプロジェクト作成・立ち上げ】)の続きです。
今回は NestJS の Controller についてまとめていきます。
前回同様、公式ドキュメントに沿ってまとめていきます。
Controller とは
Controller は、入ってくるリクエストを処理しクライアントにレスポンスを返すという責務を持ちます。
Controller の目的は、アプリケーションに送られてくる特定のリクエストを受け取ることです。
Routing 機構がコントローラーで受け取るリクエストを制御します。
各 Controller は1つ以上の route を持ち、それぞれの route はそれぞれの動きをすることができます。
Contorller を作成するには、クラスと Decorator を使用します。
Decorator はクラスとメタデータを紐づけ、 Nest にルーティングマップを作成させることができるようになります。
ルーティングマップでは、リクエストとそれに対応する Controller を紐づけます。
nest g resource [name]
で validation が実装された状態の Controller を生成できます。
デフォルトで生成される Controller を見てみる
以上の説明だけではイメージがしづらいと思うので、デフォルトで生成される Controller を少し見てみます。
デフォルトでは以下のような Controller が生成されていました。
以上の説明にのっとると、
-
@Controller()
とclass AppController
で Controller を定義 -
@Get()
とgetHello()
で route とその挙動を定義
していることがわかります。
デフォルトでは /src
直下にこの Controller が生成されます。
そのため、 npm run start:dev
などで開発サーバーを立ち上げたあと、 http://localhost:3000/
に GET リクエストを送信すると、 getHello()
メソッドが呼ばれることになります。
それ以外の要素として、 Service がありますが、こちらについてはまた別途説明します。
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
Routing
公式ドキュメントの cats.controller.ts
を使って Routing について説明していきます。
まず前述のとおり、 Controller の定義時には @Controller()
デコレータをつける必要があります。
この例では @Controller()
デコレータの第一引数に 'cats'
を渡しています。
これは path prefix と呼ばれるものです。
'cats'
という path prefix を渡すことで、 /cats
にて、 CatsController
内のHTTPリクエストメソッドデコレータ( @Get()
など)が付与されたメソッドを呼び出すことができるようになります。
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
$ nest g controller [name]
で CLI から Controller を生成できます。
@Get
HTTP リクエストメソッドデコレータは特定エンドポイントの HTTP リクエストハンドラーを作るよう Nest に指示します。
このエンドポイントは HTTP リクエストメソッドデコレータの HTTP リクエストメソッドおよびデコレータの引数で指定されたルートパスに対応します。
ルートパスとは、 コントローラーデコレータで定義されたプレフィックスと、 HTTP リクエストメソッドデコレータで与えられたパスを結合して決定されます。
この CatsController
の例では、 CatsController
で定義されるすべてのルートのパスプレフィックスとして cats
を指定しており、メソッドデコレータではパスを指定していないため、 GET /cats
エンドポイントが作成されることになります。
例えば、 CatsController
に @Get('breed')
ルートが定義された場合 GET /cats/breed
にマッピングされます。
なお、 @Get()
デコレータなどを付与するメソッド名(上の例では findAll()
など)は自由に決めることができます。
Request object
メソッドハンドラーから Express や Fastify などのプラットフォーム1で定義されたリクエストオブジェクトにアクセスできます。
リクエストメソッドへのアクセスは、 @Req
デコレータを使ってメソッドのシグネチャに注入することで可能です。
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';
}
}
他にも標準で以下のようなデコレータを使用して、以下のような HTTP リクエストのオブジェクトにアクセスできます。
以下は一部抜粋して記載しておりますので、詳しくはこちらをご参照ください。
デコレータ | オブジェクト |
---|---|
@Session |
req.session |
@Param(key?: string) |
req.params / req.params[key]
|
@Body(key?: string) |
req.body / req.body[key]
|
@Query(key?: string) |
req.query / req.query[key]
|
@Headers(key?: string) |
req.headers / req.headers[name]
|
Resources
リソース(データ)を操作するため 2 の HTTP リクエストメソッドを受けるエンドポイントは @Get()
デコレータ以外にも、 @Post()
, @Put()
, @Delete()
, @Patch()
, @Options()
で定義できる。
また、 @All()
デコレータを使うことでこれら全ての HTTP リクエストメソッドを扱うエンドポイントを定義できる。
Route wildcards
以下のようにすることでルートパスをワイルドカードで定義できる。
以下の例では、 abcd
以外にも ab_cd
, abecd
などにマッチする。
ワイルドカードは正規表現のうち、 *
, ?
, +
, ()
が使える。
-
と .
は文字列ベースのパスとして解釈される。
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}
ワイルドカードをパスの文字列間に使用できるのは使用プラットフォームが express の場合のみ。
Status code
ステータスコードはデフォルトで 200
が返されます。
ただし、 POST リクエストに対しては 201
が返されます。
@HttpCode(...)
デコレータを使うことで、任意のステータスコードを返すこともできます。
以下は、デフォルトで 201
を返す設定を 204
(No Content) を返す設定に変更しています。
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
状況に応じてステータスコードを変えたい場合は、 @Res()
などをつかってハンドラーメソッドの引数に使用プラットフォーム( express など)の response
オブジェクトを注入し、使用します。
@Post()
@HttpCode(204)
create(@Res() res: Response) {
if (...) {
res.status(500).send('Cat nap time. Try later!')
}
return 'This action adds a new cat';
}
また、エラーコードを返す場合は、以下のように例外を投げることで、 HTTP ステータスコードを変えることも可能です。
@Post()
@HttpCode(204)
create(@Res() res: Response) {
if (...) {
throw new InternalServerErrorException('Cat nap time. Try later!');
}
return 'This action adds a new cat';
}
詳しくは公式ドキュメントの Exception filters セクションをご確認ください。
Headers
レスポンスヘッダーを指定するには @Header()
もしくは、 @Res()
デコレータを使用します。
-
@Header()
デコレータを使用する例
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
-
@Res()
デコレータを使用する例
@Post()
create(@Res() res: Response) {
res.set('Cache-Control', 'none');
res.status(201).send('This action adds a new cat');
}
Redirection
こちらも @Redirect()
デコレータもしくは、 @Res()
デコレータを使用します。
@Redirect()
では、 url
と statusCode
の2つの引数を取り、どちらも省略可能です。
statusCode
のデフォルト値は 302
となっています。
@Get()
@Redict('https://nestjs.com', 301)
@Get()
find(@Res res: Response) {
if (...) {
res.redirect(301, 'https://nestjs.com');
}
}
@Redirect()
デコレータに指定したいずれの引数も以下のようにして上書きすることができます。
以下の例では、クエリパラメータに ?version=5
のような文字列が入っている場合、 /v5/
というパスへのリダイレクトに変更されています。
また、 Response
オブジェクトを使用しても、同様に挙動の変更が可能です。
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version: string, @Res() res: Response) {
if (version && version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
if (version && version == '2') {
res.redirect('https://docs.nestjs.com/v2/');
}
}
Route Parameters
GET /cats/1
の 1
部分に ID などの動的データを指定したい場合は、静的ルートでは動きません。
そのような場合は、 @Get()
などのリクエストメソッドデコレータの引数に :id
といったルートパラメータトークンをリクエストURLの設置したい箇所に追加することで動的ルートを定義できます。
また @Param()
デコレータをしようすることで、動的データをメソッドハンドラーの引数として受け取ることができます。
動的ルートは静的ルートよりうしろに定義する必要があります。
これにより、静的ルートの前に動的ルートにトラフィックが流れてしまうことを防ぎます。
以下の例では、パターン1では、 @Param() params
でパラメータを全て受け取っていますが、パターン2のように @Param('id') id
とすることで、個別にパラメータを受け取ることも可能です。
// パターン1
@Get(':id')
findOne(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
// パターン2
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}
Sub-Domain Routing
@Controller
デコレータの host
オプションにホスト名を指定することで、HTTPリクエストを行うホスト名のマッチングを行うことができます。
例えば、以下の例では curl http://localhost:3000/
としてアクセスすると 404 エラーが返ります。一方、 -H 'Host:admin.example.com'
オプションを curl
コマンドにつけることで Admin page
というレスポンスを得ることができます。
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
また、ルートパスと同様に、引数で指定するホスト名文字列にトークンを使用することで、その箇所を動的なホスト名として扱うことができます。
ホストパラメータはメソッドハンドラーにて @HostParam()
デコレータを使うことでアクセスすることができるようになります。
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
Scopes
NestJS では入ってくるリクエストを処理するにあたって、ほとんどのものが共有されます。
例えば、DBへのコネクションプールについても、シングルトンのサービスがグローバルステートで共有されます。
Node.js では、各リクエストは異なるスレッドで処理されるため、シングルトンインスタンスを使用することは完全に安全です。
ただし、 GraphQL を扱うアプリケーションでリクエストごとにキャッシュを行いたい、リクエストの追跡をしたい、アプリケーションをマルチテナントに対応したいなどの、リクエストベースのライフタイムを持つようなケースがあります。
このような場合のスコープの扱いについては、以下の詳しい内容で見ていきます。
Asynchronicity
メソッドハンドラーは、 Promise
を返す( async 関数にする)ことができます。
これにより、メソッドハンドラー内で非同期処理を行うことができます。
試しに以下のように 5 秒待つ処理を入れて、 time
コマンドで見ると、約 5 秒後にレスポンスが得られることがわかります。
@Get()
async findAll(): Promise<any[]> {
// 5秒待つ
await new Promise((resolve) => setTimeout(resolve, 5000));
return [];
} // 0.02s user 0.01s system 0% cpu 5.037 total
また、 RxJS という JavaScript で非同期処理を便利に扱うライブラリのコアな型である Observable
型を使用することもできます。
以下で使用している of
は、引数で渡した値を Observable<T>
に変換してくれる関数です。
@Get()
findAll(): Observable<any[]> {
return of([]);
}
RxJS については、以下をご参照ください。
Request payloads
以下の POST ルートハンドラはクライアントから何もデータを受け取っていません。
@Post()
create() {
return 'This action adds a new cat';
}
@Body()
デコレータを追加することでクライアントからのデータを受け取ることも可能ですが、その前に DTO (Data Transfer Object) スキーマを定義します。
DTO とは、データがどのようにネットワーク上に送られるかを定義したオブジェクトです。
DTO スキーマは TypeScript のインターフェイスを使っても定義できますが、ここではシンプルなクラスとして定義することをおすすめします。
なぜなら、 JavaScript にトランスパイルされた際、 TypeScript のインターフェイスは削除されてしまからです。
この機能は、パイプ (Pipes) が実行時に値のメタタイプにアクセスできるようにするために重要になります。
以下が DTO の例です。
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
また、以下のようにして DTO を経由して @Body()
からクライアントからのデータを取得することができます。
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
ValidationPipe
を使うことでメソッドハンドラーに渡したくないプロパティをホワイトリスト形式で取り除くことができます。
オブジェクトからはホワイトリストに指定されたプロパティのみ含まれ、指定されなかったプロパティは削除されます。
上記の CreateCatDto
の例では、 name
, age
, breed
プロパティがホワイトリストにあたります。
詳しい解説はこちらをご参照ください。
Handling errors
エラーハンドリングについては、別の章に分けられています。
こちらをご参照ください
Full resourcec sample
以下から基本的なコントローラーをつくるために使用可能なデコレータを使用した例を見ることができます。
また、以下にもサンプルコードがあるのを見つけたので、こちらもリンクを記載しておきます。
こちらは、上記サンプルとは違い、今後出てくる service のコードも含まれています。
cats.service.ts
では、サービス自体が内部プロパティとして cats
配列を持っており、そこにデータを書き込んだり、データの取得ができるようになっているようです。
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;
}
}
Nest CLI をつ使うことでボイラープレートコードを自動生成することができます。くわしくはこちらをご参照ください。
Getting up and running
上記のようにコントローラーを完全に定義しただけでは、 NestJS は CatsController
の存在を認識しておらず、コントローラークラスのインスタンスは生成されません。
以下のように AppModule
の @Module()
デコレータの controllers
配列に CatsController
を指定することでモジュールに属させることができます。
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule {}
今回は AppModule
以外を定義していないためこのような指定になっていますが、 CatsModule
に CatsController
を指定し、 AppModule
に imports
配列で CatsModule
を指定することも可能です。
このように @Module()
デコレータを使ってメタデータをモジュールクラスに付与することで、 NestJS がマウントすべき Controller を反映できます。
まとめ
本稿では NestJS の Controller について見ていきました。
ほぼ公式ドキュメントをなぞるだけになってしまい、自分なりの訳にもなっているので不正確な箇所もあるかもしれませんので、適宜公式ドキュメントも一緒にご参照ください。
次回移行も同じような形式で NestJS の機能をみていきます
-
本稿では、 Express や Fastify などの Web フレームワークのことをプラットフォームと呼びます。公式ドキュメントでは、
library-specific
などと書かれている箇所は文脈にあわせてplatform-specific
と置き換えて読んでいきます。 ↩ -
公式ドキュメントの Resource セクション にならって Resource としていますが、その意味が明確に書かれていなかったため、筆者の解釈になります。 ↩