Middlewareとは

Middlewareとは

Middlewareとは、ルートハンドラの前に呼び出される関数です。Middleware関数は、リクエストオブジェクトとレスポンスオブジェクト、そしてアプリケーションのリクエスト・レスポンスサイクルにおけるnext()Middleware関数にアクセスできます。NestJSにおけるMiddleware関数は一般的にnextという変数で示されます。
NestJSのMiddlewareは、デフォルトでExpressのMiddlewareとほぼ同じです。Expressの公式ドキュメントによると、Middlewareは以下のように説明されています。
Middlewareの機能としては、以下のようなものがあります。 ・何らかのコードを実行できる
・リクエストとレスポンスのオブジェクトを変更する
・リクエスト・レスポンスサイクルを終了するStack内の次のMiddleware関数を呼び出す
・現在のMiddleware関数がリクエストやレスポンスサイクルを終了しない場合は、next()を呼び出して次のMiddleware関数に制御を移す必要があります。そうでなければ、リクエストは放置されたままになってしまいます。
カスタムネストMiddlewareは、関数または@Injectable()デコレータを持つクラスのどちらかで実装します。クラスは、NestMiddiewareインターフェイスを実装する必要があり、関数は特に必要ないです。または、クラスメソッドを使って簡単なMiddleware機能を実装しましょう。
logger.middleware.ts
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('Request...');
next();
}
}

依存性注入(DIパターン)

NestJSのMiddlewareは、DIパターンを完全にサポートしています。ProviderやControllerと同様に、同じModule内で使えるDIパターンを実装できます。通常通り、これはconstructorで実装されます。

Middlewareを適用する

@Module()デコレータには、Middlewareを記述する場所はありません。かわりに、Moduleクラスのconfigure()メソッドで設定します。Middlewareを含むModuleは、NestModuleインターフェイスを実装する必要があります。AppModuleレベルでLoggerMiddlewareを設定しましょう。
app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
上の例では、CatsControllerの内部で定義した/catsルートハンドラに対してLoggerMiddlewareを設定しました。Middlewareを設定するときに、ルートパスとリクエストメソッドを含むオブジェクトをforRoutes()メソッドに渡すことで、さらに特定のリクエストメソッドにMiddlewareを限定できます。以下の例では、RequestMethod enumをインポートしてリクエストメソッドの種類を指定しています。
app.module.ts
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'cats', method: RequestMethod.GET });
}
}
メモ configure()メソッドはasync/awaitを使って非同期にできます。

ルートの正規表現

NestJSのMiddlewareではパターンベースのルートもサポートされています。例えば、*(アスタリスク)は任意の文字の組み合わせにマッチします。
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });ty
ab*cdルートパスは、abcdab_cdabecdなどにマッチします。ルートパスには?+*()という文字が使用でき、これらは正規表現に対応する文字のサブセットになっています。-(ハイフン)と.(ドット)は文字列ベースのパスでは文字通りに解釈されます。
注意 fastifyパッケージは最新版のpath-to-regexpパッケージを使用しており、*(アスタリスク)をサポートしなくなりました。代わりに、パラメータを使う必要があります。

MiddlewareConsumerとは

MiddlewareConsumerクラスはヘルパークラスです。Middlewareを管理するためのいくつかの組み込みメソッドを提供します。これらはすべて、流れるようなスタイルでシンプルに連鎖させることができます。forRoutes()メソッドには、単一の文字列、複数の文字列やRouteInfoオブジェクトやControllerクラスを渡すことができます。
ほとんどの場合は、Controllerの一覧をコンマで区切って渡すことになるでしょう。以下のプログラムは、Controllerを一つだけ指定した場合の例です。
app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(CatsController);
}
}
メモ apply()メソッドは、単一のMiddlewareを引数に取るか、複数のMiddlewareを引数にとって指定できます。

ルートを除外する

特定のルートに対してMiddlewareを適用しない場合、exclude()メソッドを活用します。以下のように、除外するルートを特定する一つの文字列、複数の文字列またはRouteInfoオブジェクトを渡すことができます。
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);
メモ exclude()メソッドは、path-to-regexpパッケージを使ったパラメータをサポートしています。
上記の例では、exclude()メソッドに渡された3つのルートを除き、CatsController内で定義されたすべてのルートにLoggerMiddlewareがバインドされます。

関数型Middleware

今回使用したLoggerMiddlewareクラスは非常にシンプルです。メンバもなければ、追加のメソッドもなく、依存関係もありません。どうして、クラスではなく、単純な関数で定義できないのでしょうか?
実はこれができます。このようなMiddlewareを関数型Middlewareと呼びます。ロガーMiddlewareをクラスベースから関数型Middlewareに変換してみましょう。
logger.middleware.ts
import { Request, Response, NextFunction } from 'express';
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
};
そして、これをAppModule内で使います。
app.module.ts
consumer
.apply(logger)
.forRoutes(CatsController);
メモ 依存関係を必要としないMiddlewareを作成する場合は、よりシンプルな関数型Middlewareを使うことを検討してください。

複数のMiddlewareを実装する

前述のように、複数のMiddlewareを順次実行させるためには、apply()メソッド内にコンマ区切りでリストを記述すればよいです。
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

グローバルMiddleware

登録されたすべてのルートに対して、一度にMiddlewareをバインドしたい場合はINestApplicationのインスタンスが提供するuse()メソッドを利用できます。
main.ts
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
メモ グローバルMiddlewareでDIコンテナにアクセスできません。app.use()を使用する場合、代わりに関数型Middlewareを使用できます。また、クラスMiddlewareを使用し、AppModule(または他のModule)内で.forRoutes('*')を使って実装することもできます。