仕事でNestJSを使った開発をすることになりそうなので、
NestJSとはどんな技術で、どんな機能があるのかアウトプットしながらまとめました。
NestJSの特徴
NestJSを俯瞰する
公式ドキュメントや他のブログを読んで NestJS のライフサイクルをまとめました。
APIが呼ばれるときはこういう流れで処理が実行されるということをなんとなくでも頭に入れておくと理解しやすいです。

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つの機能をカプセル化して責務を限定します。controllers
と providers
にはそれぞれモジュールが使用するコントローラとサービスを記載します。
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
PracticeModule
が PracticeController
と PracticeService
をカプセル化しているのが確認できます。
// 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設定、リクエストヘッダーの確認などを行えますが、良くも悪くもなんでもできてしまうので、
後述する Guards
や Interceptors
とどう使い分けるかプロジェクトで決めておくのが良いでしょう。
$ nest g middleware common/middleware/logger
で LoggerMiddleware
が作成されます。
以下のように追記をしてください。
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/practice
で PracticeGuard
が作成されます。
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/practice
で PracticeInterceptor
が作成されます。
以下のようにメソッド実行前後と例外で 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/practice
で PracticeFilter
が作成されるので、公式ドキュメントに倣って以下のように修正します。
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,
});
}
}
Filter
も Guards
や Interceptors
と同様に適用範囲を指定できます。
グローバルレベルでの適用
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のメリット・デメリット
メリット
デメリット
まとめ
NestJSの基本的な機能を使って簡単なAPIを作ってみました。
今回は処理の流れを確認するために各モジュールにログを書いただけでしたが、
実際には認証処理やビジネスロジックの記述や、データベースや外部サービスとの連携処理などが追加されていくはずなので、
仕事で使い始めたら応用的な技術も書き加えていこうと思います!
最後まで読んでいただきありがとうございました。
コメント