architecture

モノレポ設計パターン - Turborepo・pnpm・Nxによるスケーラブルな開発

2025.12.02

モノレポとは

モノレポ(Monorepo)は、複数のプロジェクトやパッケージを単一のリポジトリで管理するアーキテクチャパターンです。Google、Meta、Microsoftなどの大企業で採用されています。

flowchart TB
    subgraph Polyrepo["ポリレポ (Polyrepo)"]
        RA["repo-a<br/>pkg.json"] --> NPMA["npm"]
        RB["repo-b<br/>pkg.json"] --> NPMB["npm"]
        RC["repo-c<br/>pkg.json"] --> NPMC["npm"]
        RD["repo-d<br/>pkg.json"] --> NPMD["npm"]
    end

    subgraph Monorepo["モノレポ (Monorepo) - single-repo"]
        UI["packages/<br/>ui"]
        Utils["packages/<br/>utils"]
        Web["apps/<br/>web"]
        Shared["共有依存関係"]
        UI --> Shared
        Utils --> Shared
        Web --> Shared
    end

モノレポのメリット・デメリット

メリット

flowchart TB
    subgraph Benefits["モノレポの利点"]
        subgraph Share["1. コード共有の容易さ"]
            Pkg["packages/shared"] --> App["apps/web"]
            Note1["即座にインポート可能<br/>バージョン管理不要"]
        end

        subgraph Atomic["2. アトミックなコミット"]
            Note2["複数パッケージの変更を1コミットで完結<br/>破壊的変更と対応を同時にリリース"]
        end

        subgraph Unified["3. 統一されたツールチェーン"]
            Note3["ESLint, TypeScript, テスト設定を一元管理"]
        end

        subgraph Visibility["4. 依存関係の可視化"]
            Note4["パッケージ間の依存関係が明確に把握可能"]
        end
    end

デメリットと対策

デメリット対策
リポジトリサイズの肥大化Sparse checkout、シャロークローン
CIの実行時間増加差分ビルド、並列実行、リモートキャッシュ
権限管理の複雑化CODEOWNERS、ディレクトリ単位の権限設定
コンフリクトの増加適切なパッケージ分割、明確な責任範囲

ディレクトリ構成パターン

monorepo/
├── apps/                      # アプリケーション
│   ├── web/                   # フロントエンド
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── api/                   # バックエンド
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── mobile/                # モバイルアプリ
│       └── ...
├── packages/                  # 共有パッケージ
│   ├── ui/                    # UIコンポーネント
│   │   ├── src/
│   │   │   ├── Button/
│   │   │   ├── Modal/
│   │   │   └── index.ts
│   │   └── package.json
│   ├── utils/                 # ユーティリティ関数
│   │   └── ...
│   ├── config/                # 共有設定
│   │   ├── eslint/
│   │   ├── typescript/
│   │   └── tailwind/
│   └── types/                 # 共有型定義
│       └── ...
├── tools/                     # 開発ツール・スクリプト
│   ├── scripts/
│   └── generators/
├── package.json               # ルートpackage.json
├── pnpm-workspace.yaml        # ワークスペース設定
├── turbo.json                 # Turborepo設定
└── tsconfig.base.json         # ベースTypeScript設定

pnpm Workspacesの設定

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
  - "tools/*"
// ルート package.json
{
  "name": "monorepo",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.4.0"
  },
  "packageManager": "pnpm@9.0.0"
}

内部パッケージの参照

// apps/web/package.json
{
  "name": "@monorepo/web",
  "dependencies": {
    "@monorepo/ui": "workspace:*",
    "@monorepo/utils": "workspace:*",
    "react": "^19.0.0"
  }
}
// apps/web/src/App.tsx
import { Button, Modal } from '@monorepo/ui';
import { formatDate, debounce } from '@monorepo/utils';

export function App() {
  return (
    <div>
      <Button onClick={() => console.log(formatDate(new Date()))}>
        クリック
      </Button>
    </div>
  );
}

Turborepoによるビルド最適化

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "lint": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  }
}

タスク依存関係の可視化

flowchart TB
    subgraph TaskGraph["Turborepoタスクグラフ - turbo run build"]
        Types["@repo/types<br/>build"] --> Utils["@repo/utils<br/>build"]
        Utils --> UI["@repo/ui<br/>build"]
        Utils --> API["@repo/api<br/>build"]
        Utils --> Web["@repo/web<br/>build"]
    end

    Note1["^build: 依存パッケージを先にビルド"]
    Note2["並列実行: 依存関係のないタスクは同時実行"]

リモートキャッシュの設定

# Vercel Remote Cache
npx turbo login
npx turbo link

# セルフホストキャッシュ (ducktape)
# turbo.json
{
  "remoteCache": {
    "signature": true
  }
}
sequenceDiagram
    participant DevA as 開発者A<br/>(feature-1)
    participant Cache as Remote Cache
    participant DevB as 開発者B<br/>(feature-2)

    DevA->>Cache: build @repo/ui
    Cache->>Cache: hash: abc123<br/>artifacts保存

    Note over DevA,DevB: 時間経過

    DevB->>Cache: build @repo/ui
    Cache-->>DevB: キャッシュヒット!<br/>ビルドスキップ

    Note over DevA,DevB: 同一入力のビルドは即座に完了 (数秒)

Nxとの比較

観点TurborepoNx
学習コスト低い中〜高い
設定の複雑さシンプル機能豊富
ジェネレータなし充実
プラグイン限定的豊富
キャッシュVercel連携Nx Cloud
依存関係分析基本的詳細
IDE連携基本的VSCode拡張あり

選択基準:

  • シンプルさ重視 → Turborepo
  • エンタープライズ機能 → Nx
  • Vercel使用 → Turborepo
  • Angular使用 → Nx

共有設定パッケージ

// packages/config/eslint/index.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
  ],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  rules: {
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/no-explicit-any': 'warn',
  },
};

// packages/config/eslint/react.js
module.exports = {
  extends: [
    './index.js',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
  ],
  settings: {
    react: { version: 'detect' },
  },
};
// packages/config/typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true
  }
}

// packages/config/typescript/react.json
{
  "extends": "./base.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "lib": ["DOM", "DOM.Iterable", "ES2022"]
  }
}

CI/CD戦略

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # 差分検出のため

      - uses: pnpm/action-setup@v3
        with:
          version: 9

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

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm turbo run build --filter="...[HEAD^1]"
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Test
        run: pnpm turbo run test --filter="...[HEAD^1]"

      - name: Lint
        run: pnpm turbo run lint --filter="...[HEAD^1]"

フィルタリング構文

# 変更があったパッケージのみビルド
turbo run build --filter="...[HEAD^1]"

# 特定パッケージとその依存先
turbo run build --filter="@repo/web..."

# 特定パッケージとその依存元
turbo run build --filter="...@repo/ui"

# 特定ディレクトリ配下のみ
turbo run build --filter="./apps/*"

ベストプラクティス

  1. パッケージの責任を明確に: 1パッケージ1責任の原則
  2. 循環依存を避ける: 依存グラフは常にDAG(有向非巡回グラフ)に
  3. バージョニング戦略: Changesets等で統一的なバージョン管理
  4. ドキュメントの整備: 各パッケージにREADMEを配置
  5. 適切な粒度: 細かすぎず、大きすぎないパッケージ分割

参考リンク

← 一覧に戻る