NestJS CLSでAsyncLocalStorageを便利に扱う
この記事は株式会社ガラパゴス(有志) Advent Calendar 2024 の19日目です。
AsyncLocalStorageとは
AsyncLocalStorageとは、非同期処理全体を通じてデータを保持するための仕組みです。Nodeのドキュメントによると、以下のように説明されています。
継続ローカル ストレージを使用すると、状態を保存し、コールバックやプロミス チェーン全体にそれを伝播できます。Web リクエストの存続期間全体、またはその他の非同期期間全体にわたってデータを保存できます。これは、他の言語のスレッド ローカル ストレージに似ています。
要するに、リクエストのライフサイクル中にデータを保持し続けることが可能になります。
この仕組みは、例えば以下のような用途に適していそうです。
- ログの記録におけるリクエストIDの参照
- 処理の各所でユーザー情報を基にした権限判定
- リクエスト全体で共有すべきデータの保持
これにより、非同期処理における状態管理が楽に行えそうです。
NestJS CLSとは
NestJS CLSは、NestJSにおいてAsyncLocalStorageを容易に利用できるようにするためのラッパーライブラリです。公式ドキュメントでは、以下のようなユースケースが示されていました。
- ログ記録のためのリクエストID参照
- リクエスト全体におけるユーザー情報の追跡
- 認証情報や権限(ロール)の伝播によるアクセス制御
- マルチテナントアプリケーションにおける動的なデータベース接続の利用
- データベーストランザクションの伝播
- リクエストスコープがサポートされない場合の「リクエスト」コンテキストの模倣
実際の利用方法
1. インストール
まずライブラリをインストールします。本記事ではpnpmを使用しています。
pnpm add nestjs-cls
2. モジュールへの登録
次に、ClsModuleをアプリケーションのトップレベルのモジュールに登録します。以下のコード例のように、グローバル設定を有効にすると全体で利用可能となります。
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: { mount: true },
}),
],
providers: [AppService],
controllers: [AppController],
})
export class AppModule {}
3. インターセプターの作成
リクエスト情報をClsServiceに保存するためのインターセプターを作成します。今回は簡易的に試すため、HTTPリクエストヘッダーからx-role-id
を取得し、それをClsServiceに保存するような実装にしてみました。
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
@Injectable()
export class UserRoleInterceptor implements NestInterceptor {
constructor(private readonly cls: ClsService) {}
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const roleId = request.headers['x-role-id'];
console.dir(roleId);
this.cls.set('roleId', roleId);
return next.handle();
}
}
4. インターセプターの登録
忘れずにインターセプターをモジュールに登録します。
今回はグローバルスコープでインターセプターを有効化します。
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: { mount: true },
}),
],
providers: [
AppService,
{
provide: 'APP_INTERCEPTOR',
useClass: UserRoleInterceptor,
},
],
controllers: [AppController],
})
export class AppModule {}
必要に応じて、以下のようにコントローラー単位でインターセプターを適用することも可能です。
@UseInterceptors(UserRoleInterceptor)
export class UserController {}
5. 保存データの取得
インターセプターで設定したデータは、ClsServiceを介して簡単に取得することができます。
export class UsersController {
constructor(
private readonly usersService: UsersService,
private readonly clsService: ClsService, // ClsServiceを注入
) {}
async findAll() {
console.log(this.clsService.get('roleId')); // 'admin'や'user'などの値
return this.usersService.findAll();
}
}
まとめ
AsyncLocalStorageはnode v16でstableになった割と前からある仕様なのですが、
意外と知らない人も多いのではないかなーと思いました。
NestJSのモジュールの仕組みとも順応性が高くていい感じですね。
例えば、AWS API Gatewayなどを利用した際にリクエストIDや認証情報を取得したいケースではNestJS CLSがあると結構嬉しい気がします。
ぜひNestJSプロジェクトでは積極的に提案したいライブラリだと感じました!
Discussion