NestJS初心者の私「引数Decoratorって依存注入できないの?」

目次

引数DecoratorだけではDIできない

Param decorator(引数デコレーター)は依存性注入システムの外側で動作するため、
依存注入することで使えるサービスなどのクラスには直接アクセスできない。
そのため、インターセプターを介して間接的にサービスクラスを利用する。

なぜ、インターセプターなのか

インターセプターは依存性注入システムの一部になることができるため。

なぜ、インターセプターだけではできないのか

依存性注入システムの一部にはなれても、コントローラメソッドに値を返すことができないため。
逆に引数デコレーターは、DIシステムにはなれないが値を返せるので、組み合わせてつかうと良さそう。

【例】ログイン中のユーザーを返す引数デコレーター「CurrentUser」を実装

現在のリクエストオブジェクトにアクセスして、ユーザーIDを抜き出し
DBからユーザーデータを取得するのをやってくれる引数デコレーターを考えてみる。
コントローラメソッドに到達するまでにやってくれて便利そうだ。

この例をやるために、以下のライブラリが必要。

npm install cookie-session @types/cookie-session

インストール後、下記のように設定を追加。

// main.ts

const cookieSession = require("cookie-session");

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(cookieSession({ keys: ["jap98fhia2f"] }));  // 追加
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true
  }));
  await app.listen(3000);
}

bootstrap();

📘 Controller method

// GET http://localhost:3000/auth/whoami
@Get("/whoami")
@UseInterceptors(CurrentUserInterceptor)  // インターセプターを利用する宣言
whoAmI(@CurrentUser() user: User) {
  return user;
}

user にはログイン中のユーザー情報がある。ログインしてない場合は null 。
インターセプターを利用するということをデコレーターで宣言するのを忘れずに!

📘 Interceptor

@Injectable()
export class CurrentUserInterceptor implements NestInterceptor {
  constructor(private usersService: UsersService) {  // サービス依存注入。これが使いたいがために...
  }

  async intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const { userId } = request.session || {};

    if (userId) {
      const user = await this.usersService.findOne(userId);
      request.currentUser = user; // これがデコレーターに渡る
    }
    return next.handle();
  }

}

UsersService は、DBからユーザーを取得するといった、ユーザーにまつわる一連の機能を有するサービス。依存注入を介して動作する。

現在のリクエストを介してセッションにアクセスし、 userId を含む場合(ログイン中である場合)、それにマッチするユーザーをDBから取得して再びセッションに格納する。

📕 Decorator

export const CurrentUser = createParamDecorator(
  (data: never, context: ExecutionContext) => {
    const request = context.switchToHttp().getRequest();
    return request.currentUser; // NOTE: インターセプターから渡ってくる!!!
  }
);

インターセプターがセッションに格納した currentUser にアクセスしてreturnすることで、
コントローラーメソッドの引数として動作できる。

📗 Module

@Module({
  // このモジュールが提供するサービス(機能)
  providers: [UsersService, AuthService, CurrentUserInterceptor],
  // サービス(機能)を使用している(ルーティングしている)コントローラー
  controllers: [UsersController],
  // サービス内で依存しているサービスやリポジトリなど
  imports: [TypeOrmModule.forFeature([User])]
})
export class UsersModule {
}

インターセプターを使いたいモジュールのprovidersに列挙する。

Interceptorをグローバルに展開できる定義方法

以下のようにModuleに定義することで、その他のコントローラすべてで自動的に実行されるようになる。

import { APP_INTERCEPTOR } from "@nestjs/core";

@Module({
  providers: [
    UsersService,
    AuthService,
    // どのコントローラでもこのインターセプターが実行されるよう、グローバルに適用する書き方
    { provide: APP_INTERCEPTOR, useClass: CurrentUserInterceptor }
  ],
  controllers: [UsersController],
  imports: [TypeOrmModule.forFeature([User])]
})
export class UsersModule {
}

1回書くだけですべてのルートに適用できる。