Angular+NestJSのモノリポ環境をなるべくシンプルに作る


以前に投稿したAngular+NestJS+OpenAPI(Swagger)でマイクロサービスを視野に入れた環境を考えるでNestJSとAngularを連携する部分がイマイチだなーと思ってたんですが、解決策が見つかったのでシンプルなモノリポ構成で作り直してみました。

前提

  • Node.jsインストール済み
  • Angular CLIインストール済み
  • NestJSインストール済み

各種バージョン

$ node -v
v12.19.0
$ ng --version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/


Angular CLI: 10.2.0
Node: 12.19.0
OS: linux x64

Angular:
...
Ivy Workspace:

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1002.0 (cli-only)
@angular-devkit/core         10.2.0 (cli-only)
@angular-devkit/schematics   10.2.0 (cli-only)
@schematics/angular          10.2.0 (cli-only)
@schematics/update           0.1002.0 (cli-only)
$ nest --version
7.5.1

環境構築

Lernaプロジェクト作成

Lernaはモノリポ管理用のツールです。
なくても問題はないですが、サーバーとクライアントのプロジェクトに対して一括でビルドやnpm installを実行することができるので便利です。

# プロジェクトのディレクトリを作成して移動
mkdir mono-nestjs-angular && cd mono-nestjs-angular
# Lernaプロジェクトとして初期化
npx lerna init

NestJSのテンプレート生成

# 各プロジェクトはlerna initで生成されるpackagesディレクトリの下に作成する
cd packages
nest new server

Angularのテンプレート生成

ng new client --style=scss --routing=true

API側のプレフィクスを設定する

APIコールと静的ファイルのアクセスを区別するため、APIは/apiから始まるURLとなるようにプレフィクスを設定します
やり方はコチラに書いてあります

packages/server/src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    // APIのURLが/apiとなるようにプレフィクスを設定
    app.setGlobalPrefix('api');

    await app.listen(3000);
}
bootstrap();

Angularのページを静的ファイルとして返すように設定

今回やりたかったのはここですね。
リファレンスを参考に静的ファイルの設定を追加します

npx lerna add @nestjs/serve-static --scope=server
# もしくは
cd server && npm install --save @nestjs/serve-static

モジュールでServeStaticModuleをインポートします。この時のポイントは以下の通りです。

  • rootPathはAngularの出力ディレクトリと一致するようにします
    • Angularの出力ディレクトリはpackages/client/angular-cli.jsonのoutputPathに書いてあります
  • excludeにて上記で設定したプレフィクスを指定してAPIコール時は参照しないようにします
packages/server/src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

@Module({
  imports: [
    ServeStaticModule.forRoot({
        rootPath: join(__dirname, '../../client/', 'dist/client'),
        exclude: ['/api'],
      }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

ビルド&起動スクリプト追加

プロジェクト直下のlerna initで生成されたpackage.jsonに起動コマンドを追加します

package.json
{
  "name": "root",
  "private": true,
  "scripts": {
      "start": "lerna run start --scope=server --stream",
      "build": "lerna run build --stream"
  },
  "devDependencies": {
    "lerna": "^3.16.1"
  }
}
  • lerna runコマンドはプロジェクト内のpackage.jsonにあるscriptsで定義されたコマンドを一括実行します
    • startに関してはサーバーのみ実行するため、--scopeオプションでパッケージを指定する
  • --streamオプションはログ出力するためのオプションです。これがないとサーバー起動してもログが出てきません。

動作確認

プロジェクトルートで以下のコマンドを実行します

起動

npm install
npm run build
npm start

画面確認

http://localhost:3000にアクセスします

ちゃんとAngularのページが出てますね。

API確認

http://localhost:3000/apiにもアクセスしてみます

APIもコールできていますね

補足:サブディレクトリにURL直打ちで遷移できるのか

SPAの場合、index.htmlからjsで画面を遷移しているように見せているだけで、URLでサブディレクトリを指定しても実際にそのサブディレクトリにindex.htmlがあるわけではないので、普通に静的ファイルを公開するだけだとURL直打ちやサブディレクトリでF5リロードすると404になってしまいます。
(index.htmlをコピーして404.htmlを作成して対応するのが一般的か?)

ここのページでも注意書きにありますが、@nestjs/serve-staticではこの辺をうまいことやってくれるそうなので、試してみます。

ちなみに、以前書いたAngular+NestJS+OpenAPI(Swagger)でマイクロサービスを視野に入れた環境を考えるでは、app.use(regx, express.static(path))のように必ずindex.htmlを返すようにしていました。

コンポーネントを追加する

Page1ComponentとPage2Componentコンポーネントを追加します

packages/client/src/app/pages/page1.component.ts
import { Component } from '@angular/core';

@Component({
    selector: 'app-page1',
    template: `
    <h1>{{title}}</h1>
    <a routerLink="/page2">to page2</a>
    `
})
export class Page1Component {
    title = 'page1';
}

※Page2Componentはpage1の部分をpage2に変えただけ

追加したコンポーネントを参照できるようにmoduleのdeclarationsに指定します

packages/client/src/app/app.module.ts
・・・
import { Page1Component } from './pages/page1.component';
import { Page2Component } from './pages/page2.component';

@NgModule({
  declarations: [
    AppComponent,
    Page1Component,
    Page2Component,
  ],
  ・・・

ルーティングの設定をします

packages/client/src/app/app-routing.module.ts
・・・
import { Page1Component } from './pages/page1.component';
import { Page2Component } from './pages/page2.component';

const routes: Routes = [
    { path: 'page1', component: Page1Component },
    { path: 'page2', component: Page2Component },
];
・・・

画面を表示してみる

http://localhost:3000/page1にアクセスします

ちゃんと表示できました。
(下の方にPage1Componentの内容が出力されています)

一応、routerLinkd遷移できるかも確認するため、リンクを押下します

ちゃんとPage2Componentの内容に切り替わりました(URLも/page2になってますね)

まとめ

こんな感じで以前執筆した記事より簡単?にNestJSとAngularを連携させることが出来ました。
NestJSはAngularにインスパイアされただけあってSPAとの相性は良いですね^^

今回作ったものはGitHubにプッシュしてあります
https://github.com/teracy55/mono-nestjs-angular