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

以前に投稿した[Angular＋NestJS＋OpenAPI（Swagger）でマイクロサービスを視野に入れた環境を考える](https://qiita.com/teracy55/items/0327c7a170ec772970c6)で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を実行することができるので便利です。

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

## NestJSのテンプレート生成

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

## Angularのテンプレート生成

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

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

APIコールと静的ファイルのアクセスを区別するため、APIは/apiから始まるURLとなるようにプレフィクスを設定します
やり方は[コチラ](https://docs.nestjs.com/faq/global-prefix)に書いてあります

```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のページを静的ファイルとして返すように設定

今回やりたかったのはここですね。
[リファレンス](https://docs.nestjs.com/recipes/serve-static)を参考に静的ファイルの設定を追加します

```
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](http://localhost:3000)にアクセスします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/bef21784-1b7c-28e8-a0f6-3f4f1c1ba824.png)

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


## API確認

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/f4b07725-3d50-c340-e1fe-0e80ae600f7d.png)

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


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

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

[ここのページ](https://docs.nestjs.com/recipes/serve-static)でも注意書きにありますが、`@nestjs/serve-static`ではこの辺をうまいことやってくれるそうなので、試してみます。

ちなみに、以前書いた[Angular＋NestJS＋OpenAPI（Swagger）でマイクロサービスを視野に入れた環境を考える](https://qiita.com/teracy55/items/0327c7a170ec772970c6)では、`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](http://localhost:3000/page1)にアクセスします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/a40248e2-6ee0-50cc-e5a3-e56cf8c4f874.png)

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

一応、routerLinkｄ遷移できるかも確認するため、リンクを押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/5dfef87e-5c12-321f-89b1-7795b7b8bd36.png)

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


# まとめ

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

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