NestJSにおける基本概念の徹底解説
はじめに
今回の記事では、NestJSの初心者向けにNestJSで使われる基本的な概念を徹底解説する。
簡単なディレクトリ紹介
NestJSの初期ディレクトリは以下のとおりである。本記事ではsrcフォルダのファイルを中心に解説する。
src
- app.controller.spec.ts
- app.controller.ts
- app.module.ts
- app.service.ts
- main.ts
app.controller.spec.ts
Controllerのユニットテストを行う際に使うファイルである。
import { Test, TestingModule } from '@nestjs/testing'
import { AppController } from './app.controller'
import { AppService } from './app.service'
describe('AppController', () => {
let app: TestingModule
beforeAll(async () => {
app = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile()
})
describe('getHello', () => {
it('should return "Hello World!"', () => {
const appController = app.get<AppController>(AppController)
expect(appController.getHello()).toBe('Hello World!')
});
});
});
app.controller.ts
単一のrouteを持つ基本的なControllerである。
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()
}
}
app.module.ts
NestJSのアプリケーションの基本となるModuleを扱う。
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
app.service.ts
単一のメソッドを持つ基本的なServiceである。
import { Injectable } from '@nestjs/common'
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!'
}
}
main.ts
NestJSのコア関数NestFactoryを活用してNestJSアプリケーションのインスタンスを作成するアプリケーションのエントリファイルである。
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
// この処理で開発者サーバを起動できる。
async function bootstrap() {
const app = await NestFactory.create(AppModule)
await app.listen(3000)
}
bootstrap()
NestJSにおける概念の役割・実装方法
Controller
NestJSにおけるControllerは主に以下の役割を果たす。
- 形式的に
@Controller()デコレータを適用したクラス - 指定したパスでリクエストを受け取りレスポンスを返す
-
Providerが提供するサービスを利用する - 特定の
Moduleに属する
Controllerを作成するには、以下のコマンドを入力する。
nest g controller <name>
ここで、<name>には自分が作成したいControllerの名前が入る。このコマンドによって、src/<name>/<name>.controller.tsというようにテスト用のファイルが作成される。なお、Controllerに限定されないがテスト用のファイルを作成しない場合は-no--specを指定しよう。
Controllerの基本的な構造は以下のようになる。(NestJSの公式ドキュメントから引用)
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() // HTTPメソッドを指定できる。きちんとデコレータで書くこと。
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
レスポンスを特定のURLにリダイレクトしたい場合は、@Redirect()デコレータを使うか、ライブラリ固有のレスポンスオブジェクト(直接res.redirect()を呼ぶ)を使うかのどちらかになる。
@Redirect()は2つの引数を取る。それはurlとstatusCodeの2つである。第二引数を設定しない場合、デフォルトのstatusCodeの値は302に設定されている。
@Get()
@Redirect('https://nestjs.com', 301)
Controllerを使用する場合は、Moduleへ登録しなければならない。
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController], // Controller の登録
providers: [CatsService],
})
export class CatsModule {}
Providers
NestJSにおけるProviderは主に以下のような特徴を持つ。
- 形式的に
@Injectable()デコレータを適用したクラス - 依存対象として注入されるもの
-
Controllerから複雑なタスクを依頼されること
Serviceを作成する場合は以下のコマンドを入力する。
nest g service <name>
このコマンドで、src/<name>/<name>.service.tsなどのファイルが作成される。Serviceの基本的な構造は主に以下のようになる。
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;
}
}
Serviceを使用するためにはModuleへ登録しなければならない。
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService], // Service の登録
})
export class CatsModule {}
Module
NestJSにおけるModuleとは、主に以下のような特徴を持つ。
- 形式的には
@Module()デコレータを持つ -
providers,controllers,imports,exportsの4つの要素から構成される - NestJSのアプリケーションは、少なくとも1つの
Moduleを必要とし、これと他のインポートされたModuleの連鎖で成り立っている。 - 1つの特定の役割に応じて設定される
-
@Global()デコレータを適用させると、グローバルに利用できる - 使用する
Providerを動的に切り替えられる
Moduleを作成する場合は、主に以下のコマンドを入力する。
nest g module <name>
これによって、src/<name>/<name>.module.tsファイルが作成される。Moduleの基本的な構造は以下のようになる。
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController], // Controller の登録
providers: [CatsService], // Service の登録
exports: [CatsService], // エクスポートする Provider の登録
})
export class CatsModule {}
app.module.ts(NestJSのコアとなるModule)がこのModuleを扱う場合は以下のようになる。
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {}
Middleware
NestJSにおけるMiddlewareは主に以下の役割を果たす。
- Routeハンドラの前に呼び出される関数で、リクエストやレスポンスオブジェクトへとアクセスできる
- 基本的にはExpressのMiddlewareと同様
- 関数、または
@Injectable()デコレータを適用したクラスとして実装する
Middlewareを作成する場合は以下のコマンドを入力する。
nest g middleware common/middleware/<name>
Middlewareをクラスとして適用する場合の基本的な構造は以下のようになる。
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable() // @Injectable() デコレータの適用
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...'); // Middleware の処理
next(); // 次の関数へとコントロールを引き渡す
}
}
また、Middlewareを関数として定義した場合は以下のようになる。
import { Request, Response, NextFunction } from 'express';
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
}
このように、Middlewareの定義の仕方には2種類ある。公式ドキュメントにはシンプルな関数型Middlewareをなるべく使うように書かれている。
例外フィルタ(Expection Filter)
NestJSには未処理のすべての例外を処理できる例外フィルタがデフォルトで組み込まれている。
アプリのコードで処理されない例外が発生した場合、このフィルタが例外をキャッチしてレスポンスを自動で送信する。主に以下のような特徴を持つ。
例外フィルタを作成するには、以下のコマンドを実行する。
nest g filter common/filters/<name>
例外フィルタの基本的な構造は以下のようになる。
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,
});
}
}
例外フィルタは、メソッドのレベル、コントローラのレベル、グローバルなレベルで使用できる。メソッド、つまりRouteハンドラのレベルで使うためには@UseFilters()デコレータを使う。
@Post()
@UseFilters(HttpExceptionFilter) // Exception filter を登録
async create(@Body() createCatDto: CreateCatDto) {
// ...
}
コントローラレベルで使用する場合も同様に実装する。
@UseFilters(HttpExceptionFilter)
export class CatsController {}
グローバルに例外フィルタを登録するためには、@useGlobalFilters()メソッドを使う。
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(HttpExceptionFilter);
Pipe
NestJSのPipeは、@Injectable()デコレータでアノテーションされたクラスで、PipeTransformインターフェイスを実装している。
Pipeには2つの使い方がある。
- 変換:入力したデータを希望の型に変換する(例:
stringからnumberへ) - 検証:入力したデータを評価し、データが正しければ実行を継続して間違っていれば例外を投げる
NestJSには以下の9種類の組み込みのPipeが存在する。
ValidationPipeParseIntPipeParseFloatPipeParseBoolPipeParseArrayPipeParseUUIDPipeParseEnumPipeDefaultValuePipeParseFilePipe
Pipeを作成するためには、以下のコマンドを入力する。
nest g pipe common/pipes/<name>
Pipeの基本的な構造は以下のようになる。
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
なお、上記のプログラムは与えられたデータを変換するタイプのPipeだが、変換が不可能である場合は例外を投げる。これはバリデーションを行うタイプのPipeでも同様で、バリデーションのプロセスで問題があればその際も例外を投げるようにする。
Pipeは例外的に、メソッドのレベル、コントローラのレベルやグローバルなレベルに加えてパラメータのレベルでも使用できる。最初に、パラメータのレベルで使用するには以下のように、@Param()などのデコレータの内部でPipeを指定する。
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id) {
return this.catsService.findOne(id);
}
メソッドやコントローラのレベルで使用するには、以下のように@UsePipes()デコレータを活用する。
@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
// ...
}
Guard
Guardとは、@Injectable()デコレータでアノテーションされたクラスで、CanActivateインターフェイスを実装している。Guardは権限等の特定の条件に応じて、リクエストがハンドラによって処理されるべきかどうかを決定づける。
Guardを作成するには、以下のコマンドを実行する。
nest g guard common/guards/<name>
基本的な構造は以下のようになる。
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
validateRequest()関数内のロジック部分は、必要なだけシンプルにすることも洗練されたものにすることもできる。この例の主なポイントは、ガードがリクエスト/レスポンスサイクルにどのように適合するかを示すことだ。
すべてのガードはcanActivate関数を実行しなければならない。この関数では、現在のリクエストが許可されているかどうかを示すbooleanを返さなければならない。この関数は、同期または非同期(PromiseあるいはObservable経由)でレスポンスを返すことができる。NestJSはその返り値を使って、以下のようなアクションを起こす。
-
true:リクエストをそのまま実行する -
false:リクエストを拒否する
Interceptor
Interceptorとは、@Injectable()デコレータでアノテーションされたクラスでNestInterceptorインターフェイスを実装している。
Interceptorは、アスペクト指向プログラミングの手法から得た一連の便利な機能を備えている。この機能で、主に以下のようなことができる。
- メソッド実行の前後に余分なロジックをバインドする
- 関数から返される結果を変換する
- 関数からスローされる例外を変換する
- 関数の基本的な振る舞いを拡張する
- 特定の条件に応じて関数を完全にオーバーライドする
それぞれのInterceptorはinterceptメソッドを実装しており、2つの引数を取る。最初の引数はExcecutionContextのインスタンスだ。ExcecutionContextはArgumentHostを継承している。ArgumentHostは例外フィルタの章で確認できる。
Interceptorは以下のコマンドで実行される。
nest g interceptor common/interceptors/<name>
以下のプログラムはInterceptorの具体例である。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
Interceptorを使って、npmパッケージであるrxjsを使ってルートリクエストに制限時間をつけることができる。(いわゆるタイムアウトの機能)
以下のプログラムで上記の機能を実装できる。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
5秒経過すると、リクエストはキャンセルされる。RequestTimeoutExceptionを投げる前にカスタムロジックを実行できる。
まとめ
NestJSでは入門時に多種多様な概念が登場するので、それぞれの概念の役割や使い方をソースコード付きで簡潔にまとめた。NestJSで開発を進める上で重要なことは、ModuleやController、Providerなどレスポンスを返すための基本的な仕組みやコードをまとめるための構造を十分に理解する必要がある。そして、クライアントとサーバの間でのリクエスト・レスポンスサイクルを制御するための仕組みである残りの概念について、その役割や適用される順序・スコープ、コード内での組み込み方などをしっかりと理解しておこう。
余談だが、日本語でNestJSに関する体系的な知識がまとまった情報がないことをきっかけにNestJSの強化書をリリースした。(2022年9月15日時点ではまだβ版だが...)
まだ未執筆の記事がたくさんあるものの、こちらではNestJSに関する基本的な知識や応用まで幅広く解説している。言語の壁でNestJSの学習や開発を諦めるプログラマーが1人でも減るように執筆した。時間があればぜひ一読していただけると幸いである。
Discussion