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
が存在する。
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
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