この記事は NestJS Advent Calendar 2019 13日目の記事です。今日は小ネタになります。
2019 年に Web API を新規開発する場合、基本的には設計段階で API バージョニングを前提とします。バージョニングによって、Public API は勿論、Private API でも、 /v1/
から始めておき、大きな変更に応じてバージョンを変えていく事によって適切に後方互換を保つことが可能となります。
最近だと開発面だと monorepo が、インフラ面だと L7 Switch の活用で、うまい具合に古いコードベースを生かしたまま新しく書いていくこともやりやすい土壌が整っているので、バージョニングはできるだけしたいところです。
NestJS でも同様の設定を付与したいところですが、愚直に Controller のデコレータ引数でバージョン情報を付与するのは少し手間がかかりますし、ヌケモレも可能性もゼロではありません。
そんなときに便利な API バージョニングの機能を2つ紹介します。
サンプルレポジトリ
- setGlobalPrefix 版
- Nest Router 版
setGlobalPrefix の利用
最もポピュラーな設定方法です。一応オフィシャルドキュメントにもありますが、隅の隅に追いやられています。
Nest の instance にグローバルでのプレフィックスを付与するためのメソッドが生えているので、これで指定します。文字列で設定した内容が、そのまま API の prefix になります。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('v1');
await app.listen(3000);
}
bootstrap();
実際に叩くとこんな感じ。
> http get http://localhost:3000/v1
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 12
Content-Type: text/html; charset=utf-8
Date: Fri, 13 Dec 2019 10:29:31 GMT
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
X-Powered-By: Express
Hello World!
API v1 段階では、ほとんどのケースにおいてこれだけで要件を賄うことができます。特に現状込み入った仕様がない場合は、こちらで OK です。
ただし、 prefix はただ一つだけである ことに注意してください。今後v2を作る場合などに、新たに Nest インスタンスを作成する必要があります。
v1 を生かしたまま v2 を作るときになると、事前の LB やスイッチの段階でアクセスの流し先を切り替えるなど、工夫が必要となってきます。
nest-router の利用
次に、より細かなルーティングを行いたい場合は nest-router を利用する手もあります。こちらはほぼドキュメントが存在しないので、詳しく紹介します。
nest-router がマッチするシチュエーション
複数のバージョンを同居させたい場合に便利です。たとえば v1 と v2 を同時に活かしたい場合、モジュール構造は以下となります
- root.module.ts (NestFactory の create にわたすためのモジュール)
- v1.module.ts (
/v1
を受け取る nest-router のためのモジュール)- users.module.ts (
/v1/users
のためのモジュール)
- users.module.ts (
- v2.module.ts (
/v2
を受け取る nest-router のためのモジュール)
- v1.module.ts (
実例
nest-router は、 NPM / Yarn でインストールします
> yarn add nest-router
まずは v1 のエンドポイント集から作成してみます。 v1.module.ts を以下のように作成してください。
import { AppModule } from './app.module';
import { Module } from '@nestjs/common';
import { RouterModule, Routes } from 'nest-router';
const routes: Routes = [
{
path: '/v1',
module: AppModule
},
];
@Module({
imports: [
RouterModule.forRoutes(routes),
AppModule
],
})
export class V1Module { }
ポイントは routes 定義です。これまで NestJS でのリクエストの設定は Controller を Module の依存に追加することで行ってきましたが、 nest-router を利用する場合、 RouterModule と、対象ルーティングの Module の import で実現することになります。
こうすることで、 /v1/*
が AppModule へと流されます。
このままこいつを読み込んでも悪くはないのですが、 main.ts が V1Module を読み込んでいるのは少し違和感があるので、ルート用のモジュールを作ってみます。
import { Module } from '@nestjs/common';
import { V1Module } from './v1.module';
@Module({
imports: [
V1Module,
],
})
export class RootModule {}
これで v2 ができても安心ですね。
最後に main.ts を書き換えます。
import { NestFactory } from '@nestjs/core';
import { RootModule } from './root.module';
async function bootstrap() {
const app = await NestFactory.create(RootModule);
await app.listen(3000);
}
bootstrap();
これで実行してみると、問題なく /v1
がマッピングされます。
19:32:33 - Found 0 errors. Watching for file changes.
[Nest] 62284 - 2019-12-13 19:32:33 [NestFactory] Starting Nest application...
[Nest] 62284 - 2019-12-13 19:32:33 [InstanceLoader] RootModule dependencies initialized +11ms
[Nest] 62284 - 2019-12-13 19:32:33 [InstanceLoader] V1Module dependencies initialized +1ms
[Nest] 62284 - 2019-12-13 19:32:33 [InstanceLoader] RouterModule dependencies initialized +0ms
[Nest] 62284 - 2019-12-13 19:32:33 [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 62284 - 2019-12-13 19:32:33 [RoutesResolver] AppController {/v1/}: +3ms
[Nest] 62284 - 2019-12-13 19:32:33 [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 62284 - 2019-12-13 19:32:33 [NestApplication] Nest application successfully started +1ms
最後に叩いて確認。
> http get http://localhost:3000/v1
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 12
Content-Type: text/html; charset=utf-8
Date: Fri, 13 Dec 2019 10:40:44 GMT
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
X-Powered-By: Express
Hello World!
おわりに
今回は API バージョニングのときの逆引きでしたが、実際には Nest Router はより強力なルーティングを求める場合に利用すること、 setGlobalPrefix は単純にバージョンを付与するときに利用することが多くなると思うので、大体の場合は setGlobalPrefix で十分となります。
とはいえ setGlobalPrefix を使うと Health check のエンドポイントまで /v1
が付与されてしまったりして少し面倒なこともあるので、そのあたりは現場に応じてどちらを利用するか検討いただけると幸いです。
明日は @euxn23 さんがなにか書く予定です。