Help us understand the problem. What is going on with this article?

Angular+NestJS+OpenAPI(Swagger)でマイクロサービスを視野に入れた環境を考える

Angular + NestJS + OpenAPIの環境について、以前に書きました。
MEANスタックはもう古い?Angular+Nest.js+OpenAPIでTypescriptオンリー環境を構築する

ただ、この記事で書いた環境だと、クライアントとサーバーのコードが1プロジェクトになっているため、機能が増えると煩雑になりがちです。
今回はモノリポで構築しつつ、マイクロサービスも視野に入れた構成を考えます。

(2020/11/06 追記)
シンプルに作る場合の記事を新たに執筆しました。
Angular+NestJSのモノリポ環境をなるべくシンプルに作る

作る環境

イメージ

Untitled Diagram.png

プロジェクト構成

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"

クライアントプロジェクトを生成

  • client1client2を生成します
cd client
ng generate application client1 --routing --style=scss
ng generate application client2 --routing --style=scss

→通常のAngularプロジェクトと異なり、projectsディレクトリの下に各プロジェクトが生成されます。

区別がつくように画面を修正

packages/client/projects/client1/src/app/app.component.html
{{title}}
packages/client/projects/client1/src/app/app.component.ts
title = 'Hello Client1';
packages/client/projects/client2/src/app/app.component.html
{{title}}
packages/client/projects/client2/src/app/app.component.ts
title = 'Hello Client2';

サブディレクトリの設定

今回は/client1/client2でプロジェクトを分岐させるため、Angularの設定をいじる必要があります
baseHrefdeployUrlを追記します

packages/client/angular.json
    "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等をかませて参照サーバーを振り分ければ良いかと思います。

packages/server/src/main.ts
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
  • http://192.168.33.10:3000/client1にアクセス

    image.png

  • http://192.168.33.10:3000/client2にアクセス
    image.png

  • http://192.168.33.10:3000/apiにアクセス
    image.png

ちゃんと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で表示できるようにします
packages/server/src/main.ts
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クラスを作成します。
packages/server/src/dto/hello.dto.ts
import { ApiModelProperty } from '@nestjs/swagger';

export class HelloDto {
    @ApiModelProperty()
    message: string;
}
  • コントローラーで作成したDtoクラスを返すようにします
    • @ApiResponseでHelloDtoを型に指定します
packages/server/src/app.controller.ts
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にアクセスします

image.png

→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内での出力ディレクトリを指定します
packages/client/package.json
  "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内容を変更します
packages/client/projects/api-client/src/public-api.ts
/*
 * Public API Surface of api-client
 */

export * from './lib/index';

API呼び出し

各AngularプロジェクトでAPIを呼んでみます

ApiModuleのインポート

app.module.tsHttpClientModuleApiModuleをインポートします
basePathにはAPI側のURL(/api)を指定します。
※今回は同じサーバー上で動かすため、host:portは不要です。

packages/client/projects/client1/src/app/app.module.ts
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を呼んでみます
    • 取得した値をタイトルに追記してます
packages/client/projects/client1/src/app/app.component.ts
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も同様に修正します

動作確認

  • http://192.168.33.10:3000/client1にアクセス
    image.png

  • http://192.168.33.10:3000/client2にアクセス
    image.png

ちゃんとAPIで取得したメッセージが表示されています!!!

最後に

割と簡単な修正だけで、いい感じにプロジェクトの分割が出来ました。

クライアント側で共通のコンポーネントなんかもライブラリとして作成すると、簡単にプロジェクト間で共用できるため、結構汎用性の高い環境が出来たかなと思います。

実際にマイクロサービスで運用するにはもっといじらないとダメですが、最初からこのような構成で開発をしていればマイクロサービスへの移行も簡単に出来るかなと思います。

teracy55
2011年からエンジニアやってます。 最近はAngular/NestJSをメインに Webシステム作ってます。過去には、組み込みエンジニアとしてC/C++でカーナビ開発、Javaで業務システム、PHPでのWebシステム開発、Android/iOSアプリ開発(Flutter、Monaca、CocosCreator)なんかをやってきました。
https://teracy-blog.web.app/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away