Angular + NestJS + OpenAPIの環境について、以前に書きました。
MEANスタックはもう古い?Angular+Nest.js+OpenAPIでTypescriptオンリー環境を構築する
ただ、この記事で書いた環境だと、クライアントとサーバーのコードが1プロジェクトになっているため、機能が増えると煩雑になりがちです。
今回はモノリポで構築しつつ、マイクロサービスも視野に入れた構成を考えます。
(2020/11/06 追記)
シンプルに作る場合の記事を新たに執筆しました。
Angular+NestJSのモノリポ環境をなるべくシンプルに作る
作る環境
イメージ
プロジェクト構成
angular-nest
└ packages
└ server ・・・ NestJS + OpenAPIによるAPIサーバー
└ client ・・・ Angularワークスペース
└ projects
└ api-client ・・・ OpenAPIクライアントライブラリ
└ client1 ・・・ Angularプロジェクト1アプリケーション
└ client2 ・・・ Angularプロジェクト2アプリケーション
URL設計
今回はサブディレクトリで参照先を分岐させたいと思います
パス | 対象 |
---|---|
/api | NestJSのRestAPI |
/client1 | Angularプロジェクト1 |
/client2 | Angularプロジェクト2 |
環境構築
Lernaプロジェクトを生成
サーバー/クライアントを1プロジェクトで管理するため、npmプロジェクト向けモノリポ管理ツールであるLernaを導入します
公式の手順通りにLernaプロジェクトを生成します
mkdir angular-nest && cd angular-nest
npx lerna init
クライアントサイドのプロジェクト作成
マイクロサービス前提での構築とするため、クライアントサイドはMultiProjectでの構築を行います
前提
-
@angular/cli
導入済み
Angularのワークスペースを生成
cd packages
ng new client --createApplication="false"
クライアントプロジェクトを生成
-
client1
とclient2
を生成します
cd client
ng generate application client1 --routing --style=scss
ng generate application client2 --routing --style=scss
→通常のAngularプロジェクトと異なり、projectsディレクトリの下に各プロジェクトが生成されます。
区別がつくように画面を修正
{{title}}
title = 'Hello Client1';
{{title}}
title = 'Hello Client2';
サブディレクトリの設定
今回は/client1
と/client2
でプロジェクトを分岐させるため、Angularの設定をいじる必要があります
→baseHref
とdeployUrl
を追記します
"client1": {
・・・
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"baseHref": "/client1/",
"deployUrl": "/client1/",
・・・
"client2": {
・・・
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"baseHref": "/client2/",
"deployUrl": "/client2/",
ビルドします
npm run build client1
npm run build client2
サーバーサイドプロジェクトを作成
前提
-
nestjs/cli
導入済み
NestJSテンプレートプロジェクトを生成
cd packages
nest new server
→npm/yarnどちらか聞かれるので、今回はnpmで生成します
サーバーとクライアントの連携
今回はとりあえず一つのサーバー上で動かすので、NestJSでAngularプロジェクトの振り分けをします。
※実際にマイクロサービスにする場合は、Nginx等をかませて参照サーバーを振り分ければ良いかと思います。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// APIは/apiでアクセス
app.setGlobalPrefix('api');
// フロントの参照先を設定
const clientPath = __dirname + '/../../client/dist';
app.use(express.static(clientPath));
// client1プロジェクトは/client1の時に参照
app.use(/^\/client1.*$/, express.static(clientPath + '/client1/index.html'));
// client2プロジェクトは/client2の時に参照
app.use(/^\/client2.*$/, express.static(clientPath + '/client2/index.html'));
await app.listen(3000);
}
bootstrap();
動作確認
ここまでで、最小構成の環境ができたので動作確認をしてみます。
※ちなみに、この環境はVirtualBOX+Vagrantの仮想マシン上で試しています。
- サーバーを起動します
cd packages/server
npm run start
ちゃんとclient1とclient2の呼び分け、APIへのアクセスができてます!
OpenAPIの導入
NestJSのOpenAPI対応
- 公式の手順に従って、nestjs/swaggerをインストールします
npm install --save @nestjs/swagger swagger-ui-express
※packages/server下で実行してください
- main.ts修正
- DocumentBuilderおよびSwaggerModuleでOpenAPIの設定をします
-
SwaggerModule.setup
の第一引数はSwaggerUI表示用のURLのため、今回は/api/docs
で表示できるようにします
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// APIは/apiでアクセス
app.setGlobalPrefix('api');
// OpenAPI(Swagger)設定
const options = new DocumentBuilder()
.setTitle('Cats example')
.setDescription('The cats API description')
.setVersion('1.0')
.addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api/docs', app, document);
// フロントの参照先を設定
const clientPath = __dirname + '/../../client/dist';
app.use(express.static(clientPath));
// client1プロジェクトは/client1の時に参照
app.use(/^\/client1.*$/, express.static(clientPath + '/client1/index.html'));
// client2プロジェクトは/client2の時に参照
app.use(/^\/client2.*$/, express.static(clientPath + '/client2/index.html'));
await app.listen(3000);
}
bootstrap();
APIを修正
- Dtoクラスを作成します。
import { ApiModelProperty } from '@nestjs/swagger';
export class HelloDto {
@ApiModelProperty()
message: string;
}
- コントローラーで作成したDtoクラスを返すようにします
-
@ApiResponse
でHelloDtoを型に指定します
-
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { HelloDto } from './dto/hello.dto';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) { }
@Get()
@ApiResponse({ status: 200, type: HelloDto })
getHello(): HelloDto {
const message = this.appService.getHello();
return { message };
}
}
SwaggerUIの確認
npm run start
でサーバーを起動し、http://192.168.33.10:3000/api/docs
にアクセスします
→SwaggerUIが表示され、HelloDtoもModelとして認識されています
また、/api/docs-json
とすればSpecファイル(json)の内容が出力されます
Angular側のAPIクライアント自動生成
クライアント側で共通で利用するため、ライブラリ化します。
APIクライアントライブラリ生成
cd packages/client
ng generate library api-client
- ここはopenapi-generatorによる自動生成ファイル置き場となるため、ライブラリの中身を削除します
rm -rf projects/api-client/src/lib/*
openapi-generatorの導入
openapi-generatorはJavaで作られているツールのため、普通に動かそうとするとJREが必要でめんどくさいので、docker方式を採用します。
毎回コマンドを打つのはめんどくさいので、package.jsonにスクリプトを登録しておきます。
【オプションについて】
-
-i
先ほどのspecファイル表示URLを指定します -
-g
出力形式(typescript-angular)を指定します。 -
-o
docker内での出力ディレクトリを指定します
"scripts": {
・・・
"generate:api-client": "docker run --rm -v ${PWD}/projects/api-client/src/lib:/local/api-client openapitools/openapi-generator-cli generate -i http://192.168.33.10:3000/api/docs-json -g typescript-angular -o /local/api-client"
- openapi-generatorを起動します
- NestJSを起動させておき、/api/docs-jsonにアクセスできる状態にしてください
npm run generate:api-client
- openapi-generatorの出力内容に合わせてexport内容を変更します
/*
* Public API Surface of api-client
*/
export * from './lib/index';
API呼び出し
各AngularプロジェクトでAPIを呼んでみます
ApiModuleのインポート
app.module.ts
でHttpClientModule
とApiModule
をインポートします
basePath
にはAPI側のURL(/api
)を指定します。
※今回は同じサーバー上で動かすため、host:port
は不要です。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
import { ApiModule, Configuration } from 'projects/api-client/src/lib';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
ApiModule.forRoot(() => new Configuration({basePath: '/api'})),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
※client2も同様
APIを呼んでみる
- コンポーネントで
DefaultService
をインジェクションし、onInitでAPIを呼んでみます- 取得した値をタイトルに追記してます
import { Component, OnInit } from '@angular/core';
import { DefaultService, HelloDto } from 'projects/api-client/src/lib';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'client1';
constructor(private api: DefaultService) { }
async ngOnInit() {
const result: HelloDto = await this.api.rootGet().toPromise();
this.title += ' ' + result.message;
}
}
- openapi-generatorにより、APIクライアントおよびModelクラスが生成されているため、そのまま利用することができます
※client2も同様に修正します
動作確認
ちゃんとAPIで取得したメッセージが表示されています!!!
最後に
割と簡単な修正だけで、いい感じにプロジェクトの分割が出来ました。
クライアント側で共通のコンポーネントなんかもライブラリとして作成すると、簡単にプロジェクト間で共用できるため、結構汎用性の高い環境が出来たかなと思います。
実際にマイクロサービスで運用するにはもっといじらないとダメですが、最初からこのような構成で開発をしていればマイクロサービスへの移行も簡単に出来るかなと思います。