見出し画像

npm workspaceを利用してNestJS + Create React Appをモノレポ化しよう

はじめに

こんにちは。SHIFT DAAE 開発グループ所属のsakuraiです。
NestJSとReactを利用した開発を行う中、

  • おなじTypeScriptで書いているのであれば、共通化したモジュールを利用したい。

  • フロントエンドとバックエンド単位ではなく機能単位でPRを作成したい。

という思いがあふれてきたので、npm workspaceを利用してモノレポ化しました。その際に実施したことを今後の為にまとめておこうと思います。

モノレポ化の手段について

まずは、モノレポ化の手段を検討しました。

といった方法もありますが、 npm v7以降から利用できるnpm workspaceを選びました。

追加のツールが不要であることやフレームワークに依存せず、標準機能として用意されるモノだけで構築しておいた方が良さそうという判断が理由です。
(と書きましたが、他のツールにについては未調査ですので、便利そうであればそちらも今後利用してみたいです。)

モノレポ概要

今回の主な目的は

  • フロントエンドとバックエンドから共通モジュールを利用する。

  • フロントエンドとバックエンドを1つのリポジトリに統合する。

の2つであるため以下の構成としました。

# ディレクトリ構成

monorepo_pj/
├── package.json
└── packages
    ├── backend
    │   └── package.json
    ├── common
    │   └── package.json
    └── frontend
        └── package.json

構築環境

環境は下記の通りです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
Codename:       jammy

$ npm -v
8.19.3
$ node -v
v18.13.0

それでは以下に、構築手順を記載していきます。

モノレポの構築手順

ルートディレクトリの設定1

まずはプロジェクト用のルートディレクトリを作成し、
package.jsonと.npmrcファイルを作成します。

mkdir monorepo_pj
cd monorepo_pj/
touch package.json
touch .npmrc

ルートディレクトリのpackage.jsonには最低限の記載を行います。

# package.json
{
    "name": "@monorepo-project/root",
    "description": "モノレポのサンプルプロジェクトです",
    "private": true
}

モノレポを構築する場合スコープが設定可能です。
npmコマンドのオプション(--scope)で都度スコープ指定することも可能ですが、毎回指定するのも面倒ですので.npmrcファイルに設定します。

// .npmrc
scope=monorepo-project
init-author-email=author@example.comm
init-author-name=author
init-author-url=https://example.com/
init-license= UNLICENSED
init-version=0.0.0

ワークスペースの作成

下記コマンドでcommon、backend、frontendという3つのワークスペースを作成します。

npm init -w packages/common -w packages/backend -w packages/frontend
// 以降、対話式の設定は全て初期値を選択。

バックエンドの作成

NestJSの新規作成コマンドを実行します。

rm packages/backend/package.json // ワークスペース作成時のpackage.jsonが重複するため削除
npx nest new packages/backend 
//package managerはnpmを選択

フロントエンドの作成

create-react-appコマンドを実行します。

rm packages/frontend/package.json  // ワークスペース作成時のpackage.jsonが重複するため削除
npx create-react-app packages/frontend --use-npm --template typescript

package.json の名前の変更

バックエンドとフロントエンドのpackage.jsonを編集し、 スコープ名を追記します。

# packages/frontend/package.json 
{
    "name": "@monorepo-project/frontend",
}

# packages/backend/package.json 
{
    "name": "@monorepo-project/backend",
}

フロントエンドとバックエンドのポート、プロキシ設定

モノレポ自体の設定ではないですが、NestJSとReactを同時に起動した場合に3000番のポートを重複して使用しないよう、NestJSの起動ポートを変更します。
併せてReactでaxiosを利用する場合を想定してproxyの設定を追加します。

# packages/backend/src/main.ts
await app.listen(3001); // 実際に利用する場合は環境変数からポート番号を取得する事などを検討する。
# packages/frontend/package.json 
{
  "proxy": "http://localhost:3001"
}

共通モジュール(common)の設定

共通モジュールとして利用したいcommonディレクトリについても設定を行います。
まずはTypeScript関連の設定です。
tsconfig.jsonはtsc --initコマンドで生成される初期設定ファイルに型定義を出力する"declaration": trueの設定や出力ディレクトリの設定などを追加しています。

touch packages/common/tsconfig.json

# packages/common/tsconfig.json
{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "declaration": true,
    "rootDir": "src",
    "baseUrl": "./",
    "outDir": "./dist",
  }
}

次にpackage.jsonの設定を追記します。
tsconfig.jsonでコンパイル後のファイルの出力先をdistに指定した為、
indexファイルのパスを変更します。

# packages/common/package.json
{
  "main": "dist/index.js",
  "types": "dist/index.d.ts",  
}

共通モジュール(common)の作成

共通モジュールの作成準備が完了したので、
続けて共通モジュールを作成します。

今回はzod(https://zod.dev/)を利用した精査定義
バックエンドとフロントエンドで共有する想定とします。

まずはzodをインストールします。
ルートディレクトリでインストールをすると全てのワークスペースで
共通して依存関係を追加することが可能です。

npm i zod
# ./package.json
{
    "dependencies": {
        "zod": "^3.20.2" //ルートディレクトリのpackage.jsonに依存関係が追加される
    }
}

今回は実施しませんが、特定のワークスペースのみにインストールする場合は -w オプションをつけて実行します。

npm i パッケージ -w ワークスペース名

続けて、zodを利用した精査定義を作成します。

mkdir packages/common/src
touch packages/common/src/user.ts

# packages/common/src/user.ts
import { z } from 'zod';

export const User = z.object({
  last_name: z.string().min(1).max(255),
  first_name: z.string().min(1).max(255),
});
touch packages/common/src/index.ts

# packages/common/src/index.ts
export * from './user';

共通モジュールの作成が完了したので、バックエンドとフロントエンドから利用できるように設定を続けます。
その前にルートディレクトリの設定に追加が必要ですので、再びルートディレクトリの設定に戻ります。

ルートディレクトリの設定2

ルートディレクトリにビルド用の設定ファイルtsconfig.build.jsonを作成し、
package.jsonにビルド用のコマンドを追記します。

また、ルートディレクトリから各アプリを起動できるようルートのpackage.jsonのscriptsに
起動用のスクリプトを記載します。

touch tsconfig.build.json

# ./tsconfig.build.json
{
    "files": [],
    "references": [
        { "path": "packages/common" },
        { "path": "packages/backend" },
        { "path": "packages/frontend" },
    ]
}
# ./package.json
{
    "scripts": {
        "prepare": "npm run compile",
        "compile": "tsc -b tsconfig.build.json",
        "start:dev:api": "npm run start:dev -w @monorepo-project/backend",
        "start:app": "npm run start -w @monorepo-project/frontend"
    }
}

上記設定後はルートディレクトリからビルドや起動が実施可能となります。

バックエンドとフロントエンドから共通モジュールを利用する。

ビルドまで可能になったら、バックエンドとフロントエンドから共通モジュールを利用します。

npm run compile

で共通モジュールのビルドを実行し、
バックエンドとフロントエンドのpackage.jsonに共通モジュールのnameを依存関係に追記することで、共通モジュールを利用することが可能です。

# packages/backend/package.json
# packages/frontend/package.json
{
  "dependencies": {
    "@monorepo-project/common": "^0.0.0", // 依存関係に追記
  }
}

バックエンドでの利用

# packages/backend/src/app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { User } from '@monorepo-project/common'; // 共通モジュールのインポート

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    
    User.parse({}); // 共通モジュールが利用可能

    return this.appService.getHello();
  }
}

フロントエンドでの利用

# packages/frontend/src/App.tsx
import { z } from 'zod';
import { User } from '@monorepo-project/common'; // 共通モジュールのインポート

function App() {

  type User = z.infer<typeof User>; // 共通モジュールから型情報を取得

  return (
    ~ 省略 ~
  );
}
export default App;

念のため、アプリの起動まで実施できることを確認します。

npm install

// 下記を別ターミナルで起動する
npm run start:dev:api 
npm run start:app

無事モノレポ化が完了しました!

おまけ1

ルートディレクトリにtsconfig.base.jsonを作成し
各ワークスペースのtsconfig.jsonからextendsすることで
設定を共通化できます。

touch tsconfig.base.json

#  ./tsconfig.base.json
{
  "compilerOptions": {
    //共通の設定を記載する
  }
}
#  packages/common/tsconfig.json
#  packages/backend/tsconfig.json
#  packages/frontend/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
}

今回、実際には既存プロジェクトをモノレポ化しており、
既存の設定の共通化までは実施しておらず、採用しませんでした。
1から作成する場合などはまとめても良いと思います。

おまけ2

開発中、ソースを修正した場合NestJSとReactについてはホットリロードされます。
共通モジュールも同様に変更を検知して再ビルドさせるため、--watchオプションをつけてビルドしています。
このあたりは、他に良い方法がないか模索したいところです。

# ./package.json
{
    "scripts": {
        "compile:watch": "tsc -b tsconfig.build.json --watch",
    }
}

おわりに

ということで無事モノレポ化できました。
フロントエンドとバックエンドの変更を1つのPRにまとめることができるのでリポジトリ変更作業やレビューなどでモノレポの恩恵を感じることが多いです。

また実際には、既存プロジェクトをモノレポ化したのでpackage.jsonやtsconfig.jsonの設定変更にハマっています。。
まずは新規のモノレポを作成し、雰囲気をつかんだ後に既存プロジェクトをモノレポ化するとスムーズに移行できると思います。

\もっと身近にもっとリアルに!DAAE公式Twitter/


執筆者プロフィール:sakurai
SHIFT DAAE部所属の開発エンジニアです。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら 
https://recruit.shiftinc.jp/career/