飽き性の頭の中

NestJSでmiddlewareとguardとdecoratorを活用してcurrentUserを実装した話。

NestJSでmiddlewareとguardとdecoratorを活用してcurrentUserを実装した話。

tawachan
tawachan

こんにちは、たわです**。今回は NestJS でcurrentUserを使えるようにしてみたのでその方法について書きます**。

NestJS とは

assets/20200123224459.jpg

NestJS は TypeScirpt でいい感じにバックエンドを実装できるフレームワークです。

Ruby on Rails やその他の有名フレームワークと比べると、まだ新しく情報があまりネットにも落ちていないので、自分なりの実装ですが参考になればと思います

前提条件

今回の前提条件として、ユーザーの識別は header に付与された API Key によって行っていることとします。Cookie 等で行う場合もあるかもしれませんが、その場合は適宜読み替えて見てもらえればと思います。

実装方法

それでは、順番に実装方法について説明します。

ミドルウェア(middleware)を作る

まずは、currentUserを取得するために専用のミドルウェアを作ります。次のように設定しました。

ミドルウェアの定義

   // current-user.middleware.ts

import { Injectable, NestMiddleware } from "@nestjs/common";
import { UsersService } from "../services/users/users.service";
import { Response } from "express";
import { CustomRequest } from "../app.interface";

@Injectable()
export class CurrentUserMiddleware implements NestMiddleware {
  constructor(private readonly usersService: UsersService) {}
  async use(req: CustomRequest, _: Response, next: () => void): Promise<void> {
    const apiKey = req.headers["x-api-key"] as string;
    try {
      const user = await this.usersService.findByApiKey(apiKey);
      req.currentUser = user;
      next();
    } catch {
      next();
    }
  }
}

this.usersService.findByApiKey(apiKey)は実装によりけりと思いますが、DB にアクセスして該当ユーザーを取得するメソッドに置き換えてください。

こうすることでコントローラーに到達するころにはrequestの中にcurrentUserが含まれるようになります

リクエストの型を拡張

この際に、requestの型を拡張して、currentUserも含むようにしておくと便利です

   export type CustomRequest = Request & { currentUser: UserEntity };

ミドルウェアを反映

この作成したミドルウェアはapp.module.tsに入れることで反映させることができます。

   // app.module.ts

import { Module, MiddlewareConsumer } from "@nestjs/common";
import { AppController } from "./controllers/app.controller";
import { SomeOtherModule } from "./some-other.module";

@Module({
  imports: [SomeOtherModule],
  providers: [],
  controllers: [AppController],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer): void {
    consumer.apply(CurrentUserMiddleware).forRoutes("*");
  }
}

上記の場合は、ミドルウェアをすべてのパスに適応しています。そもそもcurrentUserを使わないパスにつけてしまうと無駄な処理が走ってしまいますが、とりあえず全部に適応してもよいと思います。

僕の場合は、次に説明するガード(currentUserがない場合にエラーにする)の対象とするパスを選択できるようにすることで対応する形にしました

ガード(guard)を作る

次にガードです。単にrequestの中にcurrentUserが入っているだけでは不便です。

currentUserがない場合は、x-api-keyがない、または、不正ということでコントローラーに到達する前にエラーにしてしまいましょう。

ガードを定義

ガードはミドルウェアのあとに処理されます。なので、このタイミングでcurrentUserがない場合をエラーにすることで適切にリクエストを処理できます

   // current-user.guard.ts

import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { CustomRequest } from "../app.interface";

@Injectable()
export class CurrentUserGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const request: CustomRequest = context.switchToHttp().getRequest();
    const user = request.currentUser;
    return user !== undefined;
  }
}

ガードを反映

ガードを反映させる方法はいくつかあります。

main.tsでグローバルに反映させる方法もありますし、デコレーターでコントローラーのクラスやそのメソッドに適用させることもできます。

   // app.controller.ts
// controllerのクラスに反映させる例
import { Controller, Get } from "@nestjs/common";
import { CurrentUserGuard } from "./current-user.guard";

@Controller()
@UseGuards(new CurrentUserGuard())
export class AppController {
  @Get()
  index(): string {
    return `
    <div>sample page</div>
    `;
  }
}

これでもいいのですが、こうすると付与すべきデコレーターが無限に増えて大変なので、カスタムデコレーターを使って少しでも楽になるようにしました

デコレーターを作る

少し楽をするためにカスタムデコレーターを作りました。使い方に慣れてくると色々工夫できるので面白くなってきます(ようやくちょっとわかった気がしている)。

デコレーターを定義

このような形でデコレーターを定義できます。

   // current-user-guard.decorator.ts

export function GuardCurrentUser(type: EndpointType, option?: { isUserCreation?: boolean }) {
  const userDecorators = [ApiSecurity("X-Api-Key"), UseGuards(new CurrentUserGuard())];
  return applyDecorators(...userDecorators);
}

ApiSecurity("X-Api-Key")はここで新たに追加していますが、これは OpenAPI(Swagger)の自動生成を使っている人には便利です。

2 つデコレーターを付ける代わりに@GuardCurrentUserをつけるだけで良くなります。

また**currentUserにまつわるデコレーターを変更したくなっても付けた箇所をすべてもれなく変更するのではなくこのカスタムデコレーター 1 つだけで良くなるのでその意味でもかなり便利です**。

OpenAPI(Swagger)用の設定

ApiSecurity("X-Api-Key")を定義した上で、main.tsで apidoc に次のような設定をすると、

   // main.tsの一部

const options = new DocumentBuilder()
  .setTitle("API LIST")
  .addSecurity("X-Api-Key", {
    type: "apiKey",
    in: "header",
    name: "x-api-key",
    description: "ユーザーのAPI Key",
  })
  .build();

const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup("/apidoc", app, document);

Swagger UI にて header が設定できるようになります。

assets/20200123232022.jpg

こうすることでリクエストを叩いてみるのもかなり楽になります。便利です。

デコレーターを反映

あとは作ったカスタムデコレーターを反映させていきます。方法はさきほどと同様ですが、コントローラーのクラス単位につけていくと楽だと思います。

もうひとつデコレーターを作成

ここまでですでに

  • コントローラー内でcurrentUserを取得
  • エラーの場合は即レスポンス

できます。

ですが、コントローラー内でcurrentUserを取得するのが少しまだ不便なのでもうひと工夫だけしました。

現状

現状だとこんな感じだと思います。

   import { Controller, Get, Req } from "@nestjs/common";
import { CustomRequest } from "../app.interface";

@Controller()
export class AppController {
  @Get()
  getUser(@Req() request: CustomRequest): UserDocument {
    return request.currentUser;
  }
}

わざわざ、requestを取得してその中のcurrentUserを引っ張ってくる必要があります。なので、currentUserを引っ張ってこれるデコレーターも作りました

currentUser用のデコレーター

とてもシンプルです。

   // current-user.decorator.ts
import { createParamDecorator } from "@nestjs/common";

import { CustomRequest } from "../app.interface";

export const CurrentUser = createParamDecorator((_data, req: CustomRequest) => {
  return req.currentUser;
});

こうすることでコントローラー側からは

   import { Controller, Get, Req } from "@nestjs/common";
import { CurrentUser } from "./current-user.decorator";

@Controller()
export class AppController {
  @Get()
  getUser(@CurrentUser() currentUser: UserDocument): UserDocument {
    return currentUser;
  }
}

と取得できるようになります。少し見栄えが良くなりました。

このような方法は公式に紹介されています。こうした一工夫もドキュメントに紹介されているので読み込むと色んな発見がありました。

まとめ

上記のようにすると、なんとかcurrentUserを各所で使い回せるようになりました。

各箇所にデコレーター付けて回るのも結構大変なので、共通処理はできる限り、

  • ミドルウェア
  • ガード
  • デコレーター

に逃がしてやるといい感じに NestJS は使いこなせていくのかなという気持ちになりました。

関連記事