NxでFull-stack TypeScript Monorepoを管理する実践ガイド:Build Cache、Shared Libs、CI/CD

Development tutorial - IT technology blog
Development tutorial - IT technology blog

クイックスタート:5分でNx Monorepoを作成する

Nxに移行する前、NestJS API、Next.jsフロントエンド、shared typesパッケージの3つの独立したリポジトリからなるコードベースで2週間近く苦労していました。移行が完了してから、キャッシュのおかげでビルド時間が8分から90秒に短縮されました。これが最も素早く始める方法です。

Nxをインストールして新しいワークスペースを作成する:

# フルスタックプリセットで新しいワークスペースを作成
npx create-nx-workspace@latest my-fullstack --preset=ts
cd my-fullstack

# NestJSとNext.jsのプラグインを追加
npm install --save-dev @nx/nest @nx/next

バックエンドとフロントエンドのアプリをすぐに作成する:

# NestJS APIを作成
npx nx g @nx/nest:app apps/api

# Next.jsフロントエンドを作成
npx nx g @nx/next:app apps/web

# 共有ライブラリを作成(apiとwebで共用)
npx nx g @nx/js:lib libs/shared-types

作成後のディレクトリ構造:

my-fullstack/
├── apps/
│   ├── api/          # NestJSバックエンド
│   └── web/          # Next.jsフロントエンド
├── libs/
│   └── shared-types/ # 共有型定義
├── nx.json
├── package.json
└── tsconfig.base.json

すぐに試してみる:

# 両方のアプリを並列実行
npx nx run-many -t serve -p api web

# すべてビルド
npx nx run-many -t build --all

Nxの内部動作を理解する

Project Graph — 自動依存関係マップ

Nxはワークスペース内のすべてのimport文を自動的にスキャンし、依存関係グラフを構築します。手動で宣言する必要は一切ありません。

# ブラウザで依存関係グラフを視覚的に確認
npx nx graph

apps/weblibs/shared-typesからインポートすると、shared-typesが変更された場合にwebの再ビルドが必要だとNxはすぐに認識します。このグラフがNxのaffectedビルド計算の基盤であり、CIの時間を最も節約できる部分です。次にその詳細を説明します。

実践的なShared Library

最近の5人プロジェクトで最も嫌だったバグは:バックエンドがUsercreatedAt: Dateとして定義しているのに、フロントエンドはcreated_at: stringを期待していた——フォーマットも名前も違い、ランタイムになってようやく発覚するという問題です。Shared libraryはこの問題を根本的に解決します。

libs/shared-types/src/lib/user.tsに共有インターフェースを作成する:

export interface User {
  id: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  createdAt: Date;
}

export interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

libs/shared-types/src/index.tsからエクスポートする:

export * from './lib/user';
export * from './lib/pagination';
export * from './lib/error-codes';

バックエンドとフロントエンドの両方でインポート — Nxが自動でパスエイリアスを解決:

// NestJSコントローラーで
import { User, ApiResponse } from '@my-fullstack/shared-types';

// Next.jsコンポーネントで
import type { User } from '@my-fullstack/shared-types';

このパスエイリアスはtsconfig.base.jsonに自動的に設定されます——ライブラリ生成時にNxが追加するので、手動で変更する必要はありません。

ビルドキャッシュの最適化 — 最も時間を節約できる部分

ローカルキャッシュ

Nxは入力(ソースファイル、環境変数、設定)のハッシュに基づいてすべてのタスク結果をキャッシュします。変更がなければ?タスクは数分ではなく数ミリ秒で再実行されます。

# 初回ビルド:3分かかる
npx nx build api
# ✓ api:build  [3m 12s]

# 2回目(変更なし):即座
npx nx build api
# ✓ api:build  [read from cache]  [45ms]

nx.jsonでキャッシュを設定する:

{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "lint", "test", "e2e"]
      }
    }
  }
}

Nx Cloudを使ったリモートキャッシュ

ローカルキャッシュは自分のマシンだけで有効です。チームが3人以上になると、開発者Aがビルドを完了したら、開発者Bは再ビルドせずにキャッシュを利用したくなります。

# Nx Cloudに接続(小規模チームならフリープランで十分)
npx nx connect

このコマンドを実行すると、Nxは自動でnx.jsonに設定を追加します。キャッシュがクラウドに同期されるようになり、CIも対象です。チームでNx Cloudを有効にしたところ、CIが12分から3分に短縮されました——CIが速くなったのではなく、メンバーの開発マシンでのビルドがすでにキャッシュされていたからです。

Affected — 変更されたものだけをビルドする

PRを扱う際に最もよく使う機能です:

# mainとの差分で影響を受けるプロジェクトだけをテスト
npx nx affected -t test --base=main --head=HEAD

# 影響を受けるプロジェクトだけをビルド
npx nx affected -t build --base=main --head=HEAD

# どのプロジェクトが影響を受けるか確認
npx nx affected:graph --base=main --head=HEAD

libs/shared-typesのファイルを変更した場合、Nxはapps/apiapps/webの両方がaffectedだと自動判定します。apps/apiだけ変更した場合、apps/webは影響を受けません——テストもビルドも行われません。

CI/CDの統合

Nx AffectedとGitHub Actions

.github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # nx affectedが動作するためにフルヒストリーが必要

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci

      # リモートキャッシュ使用のためNx Cloudに接続
      - uses: nrwl/nx-set-shas@v4

      - name: Lint affected
        run: npx nx affected -t lint --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}

      - name: Test affected
        run: npx nx affected -t test --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}

      - name: Build affected
        run: npx nx affected -t build --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}

nrwl/nx-set-shasアクションは適切なbase SHAとhead SHAを自動的に決定します——ハードコードする必要はありません。PRの場合はベースブランチと比較し、mainへのマージの場合は直前のコミットと比較します。

条件付きデプロイ

実際に変更されたサービスだけをデプロイする:

      - name: APIが影響を受けた場合にデプロイ
        run: |
          AFFECTED=$(npx nx show projects --affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }})
          if echo "$AFFECTED" | grep -q "api"; then
            echo "Deploying API..."
            # APIのデプロイコマンド
          fi

      - name: Webが影響を受けた場合にデプロイ
        run: |
          AFFECTED=$(npx nx show projects --affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }})
          if echo "$AFFECTED" | grep -q "web"; then
            echo "Deploying Web..."
            # Webのデプロイコマンド
          fi

実際のプロジェクトからの実践的なヒント

タグを使ってアーキテクチャ境界を強制する

Nxではプロジェクトにタグを付け、どのプロジェクトがどのプロジェクトをインポートできるかのルールを定義できます。このルールにより、開発者がデータベースのコードを誤ってフロントエンドに直接インポートするのを防ぎます——エラーはlint時に検出され、コードレビューを待つ必要はありません。

// 各アプリ/ライブラリのproject.json
{
  "tags": ["scope:api", "type:app"]
}

// libs/shared-types/project.json
{
  "tags": ["scope:shared", "type:lib"]
}

.eslintrc.jsonでルールを設定する:

{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "scope:api",
            "onlyDependOnLibsWithTags": ["scope:shared", "scope:api"]
          },
          {
            "sourceTag": "scope:web",
            "onlyDependOnLibsWithTags": ["scope:shared", "scope:web"]
          }
        ]
      }
    ]
  }
}

リソース制限付き並列タスク

# 最大3タスクを並列実行(スペックの低いマシンでのOOM防止)
npx nx run-many -t build --all --parallel=3

# 依存関係順に実行(shared-typesを先にビルドし、その後apiとweb)
npx nx run-many -t build --all

リセットが必要な場合のキャッシュ削除

# ローカルキャッシュを削除
npx nx reset

# またはキャッシュディレクトリを手動で削除
rm -rf .nx/cache

よくあるトラップ:.envファイルや環境設定を変更しても、Nxはそのファイルを追跡しないため、古いキャッシュから読み込み続けます。ビルドが成功と表示されるのにアプリの動作がおかしい場合は、nx resetしてから再実行してみてください——コマンドを打つのに2秒かかりますが、20分のデバッグより断然ましです。

新しいコードのための自動ジェネレーター

フォルダをコピー&ペーストする代わりに、ジェネレーターを使ってチームの規約に従った新しいモジュールを作成します:

# 新しいNestJSモジュールを作成
npx nx g @nx/nest:resource apps/api/src/users

# Reactコンポーネントを作成
npx nx g @nx/react:component Button --project=web

# 特定の機能向けに新しいライブラリを作成
npx nx g @nx/js:lib libs/feature-auth --directory=libs/feature-auth

数十種類の既製ジェネレーターに加えて、チーム独自のパターン向けにカスタムジェネレーターも作成できます。私はユニットテストのボイラープレートを含むNestJSモジュールを生成するものを持っています——新しいメンバーがコマンドを1つ実行するだけで正しい構造が出来上がり、誰かが規約を説明する手間もありません。

Share: