blog.tawa.me

東京から福岡移住したWeb開発の人が発信していくブログ

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

f:id:tawachan39:20200125193613j:plain

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

NestJSとは

f:id:tawachan39:20200123224459j:plain
NestJS

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

NestJS - A progressive Node.js framework

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が設定できるようになります。

f:id:tawachan39:20200123232022j:plain
Swagger UIにてheaderが設定できる

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

デコレーターを反映

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

もう一つデコレーターを作成

ここまでで既に

  • コントローラー内で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は使いこなせていくのかなという気持ちになりました。