NestJSの概要をアウトプットを通じて理解する

NestJS JS/TS/Node

仕事でNestJSを使った開発をすることになりそうなので、
NestJSとはどんな技術で、どんな機能があるのかアウトプットしながらまとめました。

NestJSの特徴

  • TypeScriptをベースにしたバックエンド開発のためのNode.jsのフレームワーク
  • Angularのアーキテクチャの影響を受けており、Expressをラップした作りになっている (どちらも使用経験がないのでピンと来ず)
  • アプリをモジューラモノリスで作れる (モノリスも選択可能)

NestJSを俯瞰する

公式ドキュメントや他のブログを読んで NestJS のライフサイクルをまとめました。
APIが呼ばれるときはこういう流れで処理が実行されるということをなんとなくでも頭に入れておくと理解しやすいです。

NestJSのライフサイクル

APIを作りながら概要を理解する

概要をさらったら実際に使って覚えるのが早いでしょう。
NestJSアプリをインストールして、簡単なAPIを作成しながら開発で最低限必要になる機能を学んでいきます。

アプリの作成

公式ドキュメントに従って Nest CLI を使用してアプリを作成します。
アプリ名はなんでもいいですが、この記事では nestjs_practice で進めます。

$ npm i -g @nestjs/cli  #パッケージ管理ツールが選択できます。自分はnpmを選びました。
$ nest new nestjs_practice
$ cd nestjs_practice
$ npm run start:dev #アプリを起動

ブラウザのURLに http://localhost:3000/ と入力して Hello World! が表示されていれば成功です。

デフォルトファイルの役割

NestJSのプロジェクトを作成すると初期のファイル構成は以下のようになっていると思います。

src  
┣ app.controller.spec.ts  
┣ app.controller.ts  
┣ app.module.ts  
┣ app.service.ts  
┗ main.ts

main.ts

main.tsはNestJSのエントリーファイルで、
NestFactory.create(AppModule) によってアプリのインスタンスを作成する重要な役割を持ちます。
初期ではenvファイルはないはずなので、3000番ポートでアプリを起動します。
http://localhost:3000/ でアプリを見れるのはそのためです。

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

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

module, controller, service

この3つはセットなのでまとめて説明します。
NestJSは1つのアプリに機能ごとに分割されたモジュールを複数持つモジューラモノリスというアーキテクチャを採用しています。
モジュールが1つの機能をカプセル化して責務を限定します。
controllersproviders にはそれぞれモジュールが使用するコントローラとサービスを記載します。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

コントローラはルーティングの責務を持ち、クライアントから送られてくるリクエストを適切なサービスへと流します。
ここでは http://localhost:3000/ で渡ってきたリクエストを AppServiceクラスのgetHelloメソッドへ流しています。

// importは省略

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

AppServiceクラスのgetHelloメソッドは Hello World! を返しているので、ブラウザでその文字が確認できます。
これは簡単な例となっていますが、実際がServiceの中で複雑なビジネスロジックを処理していきます。

// importは省略

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

つまりモジュール、コントローラ、サービスは三位一体で役割をこなします。

app.controller.spec.ts

ファイル名の末尾に spec.ts とついているものは全て単体テストを行うためのファイルです。
この先、CLIでコントローラやサービスを作成すると自動でspecファイルが作成されますが、
これはNestJSがデフォルトで Jest というテストフレームワークにを採用しているためです。

$ npm run test でテストが実行されるので何か処理やファイルを追加したり変更したときはコミット作成前に問題ないか確認しましょう。

Practiceモジュールを作成する

Practiveモジュールを作成してAPIを作ってみましょう。
以下のコマンドでモジュール、コントローラ、サービスを作成します。

$ nest g module practice
$ nest g controller practice
$ nest g service practice

PracticeModulePracticeControllerPracticeService をカプセル化しているのが確認できます。

// importは省略

@Module({
  controllers: [PracticeController],
  providers: [PracticeService],
})
export class PracticeModule {}

PracticeController は以下のように追記してください。

import { Controller, Get, Param } from '@nestjs/common';
import { PracticeService } from './practice.service';

@Controller('practice')
export class PracticeController {
  constructor(private readonly practiceService: PracticeService) {}

  @Get(':id')
  practiceDetail(@Param('id') id: number): string {
    return this.practiceService.practiceDetail(id);
  }
}

@Controller('practice') と記載することでプレフィックスを定義でき、
@Get(':id') と記載することでパスを定義できます。
@Controller と @Get がマッピングされ、http://localhost:3000/practice/:id というエンドポイントが作成されます。

PracticeService は以下のように追記します。
PracticeController からルーティングされた実際の処理はここに書きます。
今回は練習なのでパスパラメータを文字列として返すだけです。

import { Injectable } from '@nestjs/common';

@Injectable()
export class PracticeService {
  practiceDetail(id: number): string {
    return `practice: ${id}`;
  }
}

ブラウザで http://localhost:3000/practice/1 と入力すると practice: 1 が表示され、
http://localhost:3000/practice/hoge と入力すると practice: hoge が表示されると思います。

超簡素ではありますが、NestJSでAPIを作成することができました。

Middleware の作成と適用

NestJS の Middleware は、リクエストがルーティングされる前やレスポンスがクライアントに返される前に特定の処理を行うために使用します。
ロギングやCORS設定、リクエストヘッダーの確認などを行えますが、良くも悪くもなんでもできてしまうので、
後述する GuardsInterceptors とどう使い分けるかプロジェクトで決めておくのが良いでしょう。

$ nest g middleware common/middleware/loggerLoggerMiddleware が作成されます。
以下のように追記をしてください。

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('--- Middleware Start ---');
    res.on("finish", () => {
      console.log('--- Middleware End ---');
    });
    next();
  }
}

LoggerMiddleware をアプリに適用します。AppModule に以下のように追記してください。

// 一部import省略
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger/logger.middleware';

@Module({
  imports: [PracticeModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}

consumer.apply(LoggerMiddleware).forRoutes('*'); によって全てのAPIに対して LoggerMiddleware が適用されました。

APIを実行して以下のログがでていれば成功です。

--- Middleware Start ---
--- Middleware End ---

Middleware の適用範囲は
forRoutes(PracticeController) で PracticeController へのリクエストのみ、
forRoutes(’practice’)http://localhost:3000/practice へのリクエストのみ、と調整可能です。

Guards の作成と適用

NestJS の Guards は、リクエストに対して認可を行う責務を持ちます。
リクエストヘッダーに含まれるBearerトークンやJWTの検証を行えます。

$ nest g guard common/guards/practicePracticeGuard が作成されます。
console.log を追記をします。

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class PracticeGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    console.log('--- Guard Start ---');
    return true;
  }
}

PracticeGuard をアプリに適用する方法は以下の通りです。

グローバルレベルでの適用

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new PracticeGuard());

コントローラレベルでの適用

@Controller('practice')
@UseGuards(PracticeGuard)
export class PracticeController {
  // 省略
}

メソッドレベルでの適用

@Get()
@UseGuards(PracticeGuard)
practice(): string {
  // 省略
}

本記事ではグローバルレベルに適用させます。APIを実行して以下のログがでていれば成功です。

--- Middleware Start ---
--- Guard Start ---
--- Middleware End ---

Interceptors の作成と適用

NestJS の Interceptors は、メソッドの実行前と後に特定の処理を行うために使用します。
リクエストやレスポンス、そして例外の任意の形式に統一したり、メソッドの実行完了にかかる時間の計測などを行えます。

$ nest g interceptor common/interceptors/practicePracticeInterceptor が作成されます。
以下のようにメソッド実行前後と例外で console.log を履くように追記をします。

import { CallHandler, ExecutionContext, HttpException, HttpStatus, Injectable, NestInterceptor } from '@nestjs/common';
import { catchError, Observable, tap, throwError } from 'rxjs';

@Injectable()
export class PracticeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('--- Interceptor Start ---');
    return next.handle().pipe(
      tap(() => {
        console.log('--- Interceptor End ---');
      }),
      catchError((error) => {
        console.log('--- Interceptor Error ---');
        return throwError(() => new HttpException(error.message, HttpStatus.BAD_REQUEST));
      })
    );
  }
}

PracticeInterceptor をアプリに適用する方法は Guards と同様以下の通りです。

グローバルレベルでの適用

const app = await NestFactory.create(AppModule);
app
  .useGlobalGuards(new PracticeGuard())
  .useGlobalInterceptors(new PracticeInterceptor());

コントローラレベルでの適用

@Controller('practice')
@UseInterceptors(PracticeInterceptor)
export class PracticeController {
  // 省略
}

メソッドレベルでの適用

@Get()
@UseInterceptors(PracticeInterceptor)
practice(): string {
  // 省略
}

本記事ではグローバルレベルに適用させます。APIを実行して以下のログがでていれば成功です。

--- Middleware Start ---
--- Guard Start ---
--- Interceptor Start ---
--- Interceptor End ---
--- Middleware End ---

Pipes の作成と適用

NestJSの Pipes は、リクエストデータがコントローラにルーティングされる前に変換、検証を行う責務を持ちます。
パスパラメータの型変換や不適切な値のバリデーションなどを行えます。

現状のエンドポイントは http://localhost:3000/practice/hoge を受け付けてしまいますが、
文字列が渡された場合は弾いて、整数だけを受け付けるようにします。
加えてパスパラメータで渡ってくる数値は文字列型として渡ってくるので '1' => 1 への型変換もしたいです。

上記のようなよく使うものはNestJSが用意してくれています。
PracticeController を以下のように修正してください。

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'; // ParseIntPipe を追加
import { PracticeService } from './practice.service';

@Controller('practice')
export class PracticeController {
  constructor(private readonly practiceService: PracticeService) {}

  @Get(':id')
  practiceDetail(@Param('id', ParseIntPipe) id: number): string { // ParseIntPipe を追加
    return this.practiceService.practiceDetail(id);
  }
}

これで http://localhost:3000/practice/hoge を実行して、
{"statusCode":400,"message":"Validation failed (numeric string is expected)"} と弾かれれば成功です。

1 や 5 などの整数は今まで通り問題なく実行されるはずです。

Exception filters の作成と適用

NestJSの Exception filters は、アプリで発生した例外(エラー)をキャッチして特定の処理を行うために使用します。
エラーのロギングや通知などを行えます。

$ nest g filter common/filters/practicePracticeFilter が作成されるので、公式ドキュメントに倣って以下のように修正します。

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

@Catch(HttpException)
export class PracticeFilter<T> 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();

    console.log(`PracticeFilter: ${exception.message}`);

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

FilterGuardsInterceptors と同様に適用範囲を指定できます。

グローバルレベルでの適用

const app = await NestFactory.create(AppModule);
app
  .useGlobalFilters(new PracticeFilter())
  .useGlobalGuards(new PracticeGuard())
  .useGlobalInterceptors(new PracticeInterceptor());

コントローラレベルでの適用

@Controller('practice')
@UseFilters(PracticeFilter)
export class PracticeController {
  // 省略
}

メソッドレベルでの適用

@Get()
@UseFilters(PracticeFilter)
practice(): string {
  // 省略
}

本記事ではグローバルレベルに適用させます。
http://localhost:3000/practice/hoge を実行して以下のようにエラーログが吐かれていれば成功です。

--- Middleware Start ---
--- Guard Start ---
--- Interceptor Start ---
--- Interceptor Error ---
PracticeFilter: Validation failed (numeric string is expected)
--- Middleware End ---

NestJSのメリット・デメリット

メリット

  • バックエンドとフロントエンドをTypeScriptで統一できるのでエンジニアの確保がしやすい
  • モジューラモノリスで開発できるので大人数や複数チームでの開発が進めやすい
  • 依存性注入やデフォルトでインストールされてるJestのおかげでテストがしやすい

デメリット

  • 日本語の記事が少ない
  • middleware, Interceptors, Guards などで、どこになんの責務を持たせるか決めておかないと実装者によってバラバラになる
    (どのフレームワークにも当てはまる…)

まとめ

NestJSの基本的な機能を使って簡単なAPIを作ってみました。
今回は処理の流れを確認するために各モジュールにログを書いただけでしたが、
実際には認証処理やビジネスロジックの記述や、データベースや外部サービスとの連携処理などが追加されていくはずなので、
仕事で使い始めたら応用的な技術も書き加えていこうと思います!
最後まで読んでいただきありがとうございました。

コメント

タイトルとURLをコピーしました