📌

NestJS CLSでAsyncLocalStorageを便利に扱う

2024/12/19に公開

この記事は株式会社ガラパゴス(有志) 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