Angular＋Nest.jsのモノリスプロジェクトを、Lernaを使いプロジェクト内でクライアント／サーバーのコード分割をしてみる

現在、Angular＋Express.jsのモノリスな環境で作業を行っているのですが、クライアントサイド／サーバーサイドともにコードが大きくなってきて、メンテナンスが大変になってきました。。。

先日、[ng-japan2019](https://ngjapan.org/)に参加した際に、Angular＋Nest.jsプロジェクトを[Lerna](https://github.com/lerna)でモノリポ管理しているという話を聞き、これは良さそうだということで試してみることにしました。

※ng-japan2019の発表の様子はYouTubeで見ることができます。
[NestJS + TypeORM + Angularで作るPure TypeScriptなアーキテクチャ by 白石 俊平](https://www.youtube.com/watch?v=2QTumr8-KhU&list=PLkUrrnO0bbigFIWqrhpG-N6pZw9jfRwjO&index=11&t=0s)


Express.js⇒Nest.jsへの載せ替えもしたいので、お試しがてらAngular＋Nest.jsのモノリスプロジェクトをクライアント／サーバーでソースコードを分けつつ、モノリポで管理できるようにしたいと思います。



# 環境

VirtualBox ＋ VagrantでゲストOSとしてCentOSのイメージを使っています


# Lernaのインストール

[公式のGetting Started](https://github.com/lerna/lerna#getting-started)に従って、Lernaのインストールおよびテンプレートの作成を行います。

```
$ mkdir lerna-repo && cd $_
$ npx lerna init
```

いちいちnpxで実行するのはめんどくさいのでグローバルにLernaをインストールします。

```
npm install -g lerna
```


# クライアント／サーバー用のパッケージを追加

まずはクライアントサイドのパッケージを追加します。

```
lerna create client
```
色々聞かれますが、とりあえず何も入力せずEnter連打します。


続いてサーバーサイドのパッケージを追加します。

```
lerna create server
```
こちらも同様にEnter連打します。

以下のようなディレクトリとファイルが作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/cd11a8de-0c8d-55d7-b359-b3e45dbd75fd.png)



# Nest.jsプロジェクト作成

Nest.jsのCLIをインストールしていない場合はインストールします。

```
npm install -g @nestjs/cli
```

Nest.jsのテンプレートプロジェクトを生成し、Lernaパッケージを追加した場所にコピーします。

```
nest new server
mv server/* packages/server/
```


# Angularプロジェクト作成

AngularのCLIをインストールしていない場合はインストールします。

```
npm install -g @angular/cli
```

```
ng new client
mv client/* packages/client/
```

# 動くようにする

今のままだと、単純にNest.jsとAngularのプロジェクトがそれぞれ追加されただけなので、動くように修正していきます。

## ビルド先の設定

クライアント／サーバーともに`dist`ディレクトリにビルドファイルの出力を行うため、親の方にdistディレクトリを作成し、シンボリックリンクを作成します。

※この設定（シンボリックリンク）がなくても後述の「Nest.js ⇔ Angular連携」で直接packages/client/distを参照するようにすればいけます。

```
mkdir dist
ln -s ../packages/client/dist/client dist/client
ln -s ../packages/server/dist dist/server
```
※lerna-repoディレクトリ直下で実行します

試しにビルドしてみます

```
lerna run build
```
⇒クライアント／サーバー両方の`npm run build`が実行され、`lerna-repo/dist`内は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/b1416623-d077-990a-7e94-3e90dc483609.png)

## 起動設定

親のpackage.jsonに起動コマンドを追加します。

```lerna-repo/package.json
"scripts": {
  "start": "node dist/server/main.js"
}
```

この状態で`npm start`を実行するとサーバーが起動します。
が、このままだと、ただNest.jsのAPIを呼ぶだけでAngularが動かないので連携させます。


# Nest.js ⇔ Angular連携

サーバーサイドでAngularコードを参照できるようにします。
また、APIのURLは`/api/～`となるようにプレフィクスを設定しておきます。

```lerna-repo/server/src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// 追加 start
import * as express from 'express';
import { join } from 'path';
// 追加 end

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

// 追加 start
  app.setGlobalPrefix('api');

  app.use(express.static(join(__dirname, '..', 'client')));
  app.use('/*', express.static(join(__dirname, '..', 'client/index.html')));
// 追加 end

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

Angularとの連携に必要な作業はこれだけです！


## 起動してみる

`npm start`で起動し、ブラウザでアクセスします!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/bebcf5d3-181d-9030-c6f8-99fa840736cc.png)
※VMを`192.168.33.10`で立ち上げてます。

無事、Nest.jsにアクセスしてAngularの画面を表示することができました！！！
ちなみに`http://192.168.33.10:3000/api`にアクセスすると、Nest.js側のAPIを呼び出せます。（Hello World!!が返ってくるだけ）


## AngularからNest.jsのAPIを呼んでみる

せっかくなので、AngularからNest.jsのAPIを呼んでみます。

HttpClientを使うのでModuleをインポートします。

```lerna-repo/packages/client/src/app/app.module.ts
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    ・・・
    HttpClientModule
  ],
})
export class AppModule { }
```

`app.component.ts`のngOnInitでHttpClientを使ってAPIを呼び出してtitleにセットします。

```lerna-repo/packages/client/src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'client';

  constructor(private http: HttpClient) { }

  async ngOnInit() {
    const result = await this.http.get('/api').toPromise();
    this.title = result['message'];
  }
}
```

サーバーサイドはJSON形式でオブジェクトで返却するようにします。

```lerna-repo/packages/server/src/app.controller.ts
  @Get()
  getHello() {
    return { message: this.appService.getHello()};
  }
```

## 再度ブラウザで確認

サーバーを再起動し、再度ブラウザで確認してみます。

```
lerna run build
npm start
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/987732fe-df7a-4ad8-3c74-eb310ec9cbe0.png)

ちゃんとタイトル部分が「Hello World!!」に変わってます:grin:


# まとめ

これで、サーバーサイドとクライアントサイドのコード管理を分けつつ、モノリスな環境で動かすことが出来ました！
コード分割により、package.jsonがスッキリしたり、コードが見やすくなったり、と結構いい感じです:smiley:

デバッグをどうするかとかも考えないといけないですが、それはおいおいやって行こうかと思います。


# 参考

[Lerna](https://github.com/lerna)
[ng-japan2019](https://ngjapan.org/)
[NestJS + TypeORM + Angularで作るPure TypeScriptなアーキテクチャ by 白石 俊平](https://www.youtube.com/watch?v=2QTumr8-KhU&list=PLkUrrnO0bbigFIWqrhpG-N6pZw9jfRwjO&index=11&t=0s)