NestJSでmiddlewareとguardとdecoratorを活用してcurrentUserを実装した話。
こんにちは、たわです**。今回は NestJS でcurrentUser
を使えるようにしてみたのでその方法について書きます**。
NestJS とは
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 が設定できるようになります。
こうすることでリクエストを叩いてみるのもかなり楽になります。便利です。
デコレーターを反映
あとは作ったカスタムデコレーターを反映させていきます。方法はさきほどと同様ですが、コントローラーのクラス単位につけていくと楽だと思います。
もうひとつデコレーターを作成
ここまでですでに
- コントローラー内で
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 は使いこなせていくのかなという気持ちになりました。