LoginSignup
2
1

【NestJS】ガチ入門【その2: Controller 編】

Posted at

はじめに

前回( 【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 がありますが、こちらについてはまた別途説明します。

/src/app.controller.ts
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() など)が付与されたメソッドを呼び出すことができるようになります。

cats.controller.ts
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() では、 urlstatusCode の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/11 部分に 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 配列を持っており、そこにデータを書き込んだり、データの取得ができるようになっているようです。

cats.service.ts
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 以外を定義していないためこのような指定になっていますが、 CatsModuleCatsController を指定し、 AppModuleimports 配列で CatsModule を指定することも可能です。

このように @Module() デコレータを使ってメタデータをモジュールクラスに付与することで、 NestJS がマウントすべき Controller を反映できます。

まとめ

本稿では NestJS の Controller について見ていきました。
ほぼ公式ドキュメントをなぞるだけになってしまい、自分なりの訳にもなっているので不正確な箇所もあるかもしれませんので、適宜公式ドキュメントも一緒にご参照ください。
次回移行も同じような形式で NestJS の機能をみていきます :bow:

  1. 本稿では、 Express や Fastify などの Web フレームワークのことをプラットフォームと呼びます。公式ドキュメントでは、 library-specific などと書かれている箇所は文脈にあわせて platform-specific と置き換えて読んでいきます。

  2. 公式ドキュメントの Resource セクション にならって Resource としていますが、その意味が明確に書かれていなかったため、筆者の解釈になります。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1