Turborepo + pnpm workspaceでMonorepo構築完全ガイド〜キャッシュ・並列ビルド・Next.js連携〜

apps/packages/を分けたい」「ビルドが遅すぎる」「型の共有でts設定の地獄に陥った」——複数のNext.js/Remix/Expressアプリを束ねて運用するチームにとって、Monorepoは避けて通れません。本記事はTurborepo 2.x + pnpm workspaceを軸に、初期セットアップからキャッシュ・並列ビルド・Remote Caching・CI/CD・Next.js連携まで、コピペで動く実コード40本以上で解説する完全ガイドです。Lerna/Nxとの違いも実コードベースで比較します。

※ 個別フレームワーク詳細は姉妹記事 frameworks一覧 を参照してください。本記事は「複数アプリ・複数パッケージを束ねる土台」の話に集中します。

  1. Turborepoの全体像 — 2026年のMonorepo事情
    1. なぜTurborepoなのか — Lerna/Nxとの立ち位置
    2. 本記事のゴール構成
    3. 必要バージョン早見表(2026年5月時点)
  2. 初期セットアップ — create-turboから始める
    1. 事前準備:Node.jsとpnpmのインストール
    2. create-turboで新規プロジェクト
    3. 生成された初期構造を確認
    4. 初回ビルドで体感する
  3. pnpm workspaceの設定 — パッケージ解決の土台
    1. pnpm-workspace.yaml の基本
    2. ルートのpackage.json
    3. 内部パッケージの命名規則
    4. workspace依存の書き方:workspace:* プロトコル
    5. 追加の依存をピンポイントで入れる
  4. turbo.json 完全解剖 — タスクを設計する
    1. 最小構成のturbo.json
    2. dependsOn の3つのモード
    3. inputs と outputs:キャッシュ精度を決める
    4. 環境変数の扱い:env と passThroughEnv
    5. 並列実行と永続タスク:persistent
  5. キャッシュシステム — Local CacheとRemote Caching
    1. Local Cacheの仕組みを覗く
    2. キャッシュキーは何で構成されるか
    3. キャッシュを意図的に無効化する
    4. Remote Caching:チーム全体でキャッシュ共有
    5. Self-hosted Remote Cache(S3互換)
    6. CIだけアップロード/ローカルはダウンロードのみ
  6. フィルタと並列実行 — 大規模化を支える機能
    1. –filterの基本パターン10連発
    2. 並列度の制御:–concurrency
    3. topological(依存順序)実行の仕組み
    4. UI mode(TUI)— Turborepo 2.xの目玉機能
  7. internal packages — 型と設定を共有する
    1. tsconfig共有パッケージ
    2. ESLint共有設定(Flat Config時代)
    3. Tailwind共有設定
    4. Vitest共有設定
    5. Storybook共有設定
  8. shared/ui パッケージ — コンポーネントを共有する
    1. packages/ui の構造
    2. exports field で複数エントリーポイント
    3. Button コンポーネント本体
    4. Next.js側でtranspilePackages
    5. 使う側のコード
  9. shared/utils パッケージ — ロジックを共通化する
    1. packages/utils の構造
    2. 環境変数のスキーマ検証(Zod)
    3. 共通APIクライアント
  10. Next.js × Turborepo 実戦パターン
    1. apps/web(メインサイト)のpackage.json
    2. apps/docs(ドキュメント)のpackage.json
    3. 両方を同時起動する
    4. Next.js App RouterからUIコンポーネントをImport
  11. Remix × Turborepo
    1. Remixアプリの追加
    2. 共通UIをRemixで使う
    3. RemixのserverDependenciesToBundle設定
  12. Express × Turborepo — APIサーバーを共存させる
    1. apps/api(Express)の初期化
    2. Expressコード本体
    3. 型を共有する:packages/types
  13. CI/CD — GitHub Actionsでキャッシュを最大化する
    1. GitHub Actions基本ワークフロー
    2. 差分ビルド:変更があったアプリだけビルド
    3. matrixでパッケージごとに並列ジョブ
    4. turboのキャッシュをGitHub Actions Cacheで保存
  14. Vercelデプロイ — Turborepo公式の最短ルート
    1. 各アプリを個別Projectで設定
    2. vercel.json で設定を固定
    3. turbo-ignore で不要なデプロイをスキップ
  15. Cloudflare Pages / Workersへのデプロイ
    1. Cloudflare Pages(Next.js)
    2. Cloudflare Workers(Hono API)
  16. Nx・Lernaとの比較 — どれを選ぶべきか
    1. 機能比較マトリクス
    2. Nxと同じことをTurborepoで書くと?
    3. 選び方の指針
  17. トラブルシューティング — よくある詰まりポイント
    1. 1. キャッシュが効かない
    2. 2. transpilePackagesが効かない
    3. 3. 型が解決できない:moduleResolutionの罠
    4. 4. pnpm install後にlockfile差分が出る
    5. 5. turbo dev で複数アプリのログが混ざる
    6. 6. Vercelでmonorepoの正しいRoot Directoryが選べない
    7. 7. circular dependencyで詰まる
    8. 8. PNPMのpeer dependencies警告を消す
  18. 運用Tips — チームで使うための小ワザ集
    1. 1. Changeset でversion管理
    2. 2. husky + lint-staged でcommit時に検証
    3. 3. VS Code workspaceの設定
    4. 4. turbo runの実行時間を可視化
  19. 学習ロードマップと案件獲得
    1. 3週間で習熟するためのプラン
    2. ポートフォリオに載せるべき成果物
    3. 案件で問われる定番質問
    4. 独学が辛いと感じたら
  20. まとめ — 2026年のMonorepo戦略

Turborepoの全体像 — 2026年のMonorepo事情

2025年に Turborepo 2.x がリリースされ、$TURBO_DEFAULT$ 構文・UI mode(TUI)・改善されたフィルタなど、運用面が大きく改善されました。pnpm workspaceとの組み合わせが事実上のデファクトです。

なぜTurborepoなのか — Lerna/Nxとの立ち位置

  • Turborepo: Vercel製。turbo.json 1ファイルで完結。学習コスト低・キャッシュ強力
  • Nx: Nx製。プラグイン豊富・コードジェネレータ強力だが設定が膨大
  • Lerna: 元祖。Nx社がメンテ中。publish特化・ビルドオーケストレーションは弱い
  • pnpm workspace単体: パッケージ解決のみ。タスクオーケストレーションは別途必要

本記事のゴール構成

最終的に以下のディレクトリ構造を構築します。コピペで動かしながら読み進められます。

my-monorepo/
├── apps/
│   ├── web/          # Next.js 15
│   ├── docs/         # Next.js (Documentation)
│   ├── api/          # Express
│   └── admin/        # Remix
├── packages/
│   ├── ui/           # 共通UIコンポーネント
│   ├── utils/        # 共通ユーティリティ
│   ├── eslint-config/
│   ├── tsconfig/
│   ├── tailwind-config/
│   └── vitest-config/
├── pnpm-workspace.yaml
├── turbo.json
└── package.json

必要バージョン早見表(2026年5月時点)

ツール 推奨バージョン 備考
Node.js 20.x LTS or 22.x 22.x推奨
pnpm 9.x corepack enable で導入
Turborepo 2.3.x 以上 turbo.json v2 schema
TypeScript 5.5 以上 moduleResolution: “bundler”

初期セットアップ — create-turboから始める

まずは公式テンプレートで雛形を作り、構造を理解しましょう。create-turboはpnpmを自動選択してくれます。

事前準備:Node.jsとpnpmのインストール

# Node.js 22をvoltaで管理
volta install node@22

# corepackでpnpm 9を有効化
corepack enable
corepack prepare pnpm@latest --activate

# バージョン確認
node -v   # v22.x
pnpm -v   # 9.x

create-turboで新規プロジェクト

# 最新版のTurborepoテンプレートを生成
pnpm dlx create-turbo@latest my-monorepo

# プロンプトの選択肢
# ? Which package manager do you want to use? → pnpm
# ? Where would you like to create your Turborepo? → ./my-monorepo

cd my-monorepo
pnpm install

生成された初期構造を確認

my-monorepo/
├── apps/
│   ├── docs/            # Next.js
│   └── web/             # Next.js
├── packages/
│   ├── eslint-config/
│   ├── typescript-config/
│   └── ui/              # 共通UI
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json

初回ビルドで体感する

# 初回ビルド(キャッシュなし)
pnpm turbo build

# 2回目ビルド(キャッシュヒット)→ 数秒で完了
pnpm turbo build
# ✔ web:build  cache hit, replaying logs
# ✔ docs:build cache hit, replaying logs
# Tasks:    2 successful, 2 total
# Cached:   2 cached, 2 total
# Time:     1.2s >>> FULL TURBO

FULL TURBOと表示されればキャッシュが完全に効いている証拠です。1回目で30秒かかったビルドが2回目で1秒——これがTurborepoの最大の魅力です。

pnpm workspaceの設定 — パッケージ解決の土台

Turborepoは「タスクオーケストレーション」を担当し、パッケージ解決はpnpm workspaceが担います。両者の役割分担を理解しましょう。

pnpm-workspace.yaml の基本

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
  - "tooling/*"
  # 除外したい場合
  - "!**/test/**"

ルートのpackage.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "typecheck": "turbo run typecheck",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^2.3.0",
    "typescript": "^5.5.0",
    "prettier": "^3.3.0"
  },
  "packageManager": "pnpm@9.12.0",
  "engines": {
    "node": ">=20"
  }
}

内部パッケージの命名規則

scope付き(@my-monorepo/uiのような形式)を強く推奨します。npm公開しなくてもscopeは衝突防止に有効です。

// packages/ui/package.json
{
  "name": "@my-monorepo/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts",
    "./button": "./src/button.tsx",
    "./card": "./src/card.tsx"
  },
  "scripts": {
    "lint": "eslint .",
    "typecheck": "tsc --noEmit"
  }
}

workspace依存の書き方:workspace:* プロトコル

// apps/web/package.json
{
  "name": "web",
  "private": true,
  "dependencies": {
    "@my-monorepo/ui": "workspace:*",
    "@my-monorepo/utils": "workspace:*",
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@my-monorepo/eslint-config": "workspace:*",
    "@my-monorepo/tsconfig": "workspace:*"
  }
}

追加の依存をピンポイントで入れる

# apps/web にだけ next を追加
pnpm add next --filter web

# packages/ui に react を peer dependency として追加
pnpm add react --save-peer --filter @my-monorepo/ui

# ルートにのみ devDependency を追加
pnpm add -Dw prettier

# 全workspaceに型定義を追加
pnpm add -D @types/node -r

turbo.json 完全解剖 — タスクを設計する

turbo.jsonはTurborepoの心臓部です。Turborepo 2.xではpipelinetasksにリネームされ、$TURBO_DEFAULT$などの新構文が導入されました。

最小構成のturbo.json

{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    },
    "clean": {
      "cache": false
    }
  }
}

dependsOn の3つのモード

  • "^build"(キャレット): 依存パッケージのbuildタスクが先に完了することを要求
  • "build"(プレフィックスなし): 同一パッケージ内のbuildタスクに依存
  • "app#build"(#区切り): 特定パッケージのタスクに依存(横断的)
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"]
    },
    "deploy": {
      "dependsOn": ["build", "test", "lint"]
    },
    "e2e": {
      "dependsOn": ["web#build", "api#build"]
    }
  }
}

inputs と outputs:キャッシュ精度を決める

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": [
        "$TURBO_DEFAULT$",
        ".env.production",
        "!**/*.test.ts",
        "!**/*.stories.tsx"
      ],
      "outputs": [
        ".next/**",
        "!.next/cache/**",
        "dist/**",
        "storybook-static/**"
      ]
    }
  }
}

$TURBO_DEFAULT$はTurborepo 2.0で導入された便利な構文で、「git管理下の全ファイル(.gitignore除く)」を指します。これに追加ファイルや除外を重ねるのが2026年のベストプラクティスです。

環境変数の扱い:env と passThroughEnv

{
  "tasks": {
    "build": {
      "env": ["NEXT_PUBLIC_*", "DATABASE_URL", "NODE_ENV"],
      "passThroughEnv": ["GITHUB_TOKEN", "VERCEL_*"]
    }
  },
  "globalEnv": ["NODE_ENV", "CI"],
  "globalPassThroughEnv": ["AWS_*"]
}
  • env: キャッシュキーに含まれる(値が変わるとキャッシュミス)
  • passThroughEnv: タスクには渡すが、キャッシュキーには含めない(トークン類)
  • globalEnv/globalPassThroughEnv: 全タスク共通

並列実行と永続タスク:persistent

{
  "tasks": {
    "dev": {
      "cache": false,
      "persistent": true,
      "interactive": true
    },
    "watch": {
      "cache": false,
      "persistent": true
    }
  }
}

persistent: trueturbo devで複数アプリを並列起動するときに必須です。「タスクが終わらないこと」を明示的に宣言する設定です。

キャッシュシステム — Local CacheとRemote Caching

Turborepoのキャッシュは「inputs hash → outputsをzstd圧縮して保存」というシンプルかつ強力な仕組みです。理解すればCI時間を桁違いに短縮できます。

Local Cacheの仕組みを覗く

# キャッシュは ~/.turbo or node_modules/.cache/turbo に保存
ls node_modules/.cache/turbo/

# 詳細ログを見る
TURBO_LOG_VERBOSITY=2 pnpm turbo build

# どのファイルがハッシュに含まれているか確認
pnpm turbo build --dry-run=json | jq '.tasks[0].inputs'

キャッシュキーは何で構成されるか

キャッシュキー = SHA256(
  - inputsで指定されたファイル群の内容
  - envで指定された環境変数の値
  - turbo.jsonの該当タスク定義
  - 依存パッケージのアウトプットhash
  - グローバル設定(globalDependencies等)
)

キャッシュを意図的に無効化する

# 強制的に再ビルド
pnpm turbo build --force

# 特定パッケージのキャッシュだけ破棄
pnpm turbo build --filter=web --force

# キャッシュディレクトリを物理削除
rm -rf node_modules/.cache/turbo .turbo

Remote Caching:チーム全体でキャッシュ共有

これがTurborepoの最大の差別化要素です。Vercel公式のRemote Cacheを無料で使えます。チームメンバーAがビルドしたキャッシュを、メンバーBやCIが利用できるようになります。

# Vercelアカウントとリンク
pnpm turbo login

# Remote Cacheにリンク
pnpm turbo link

# 以後、ビルド結果が自動でアップロード/ダウンロードされる
pnpm turbo build
# ✔ web:build  cache hit, replaying logs (remote)
# Remote caching enabled

Self-hosted Remote Cache(S3互換)

# Cloudflare R2やAWS S3でセルフホストする例
# 環境変数で接続先を指定
export TURBO_API="https://my-cache-server.example.com"
export TURBO_TOKEN="my-secret-token"
export TURBO_TEAM="my-team"

pnpm turbo build --remote-cache-timeout=30

CIだけアップロード/ローカルはダウンロードのみ

# ローカルはダウンロード専用(誤った汚染を防ぐ)
pnpm turbo build --cache=remote:r,local:rw

# CIではフル読み書き
pnpm turbo build --cache=remote:rw,local:rw

フィルタと並列実行 — 大規模化を支える機能

Monorepoが大きくなると「特定アプリだけ」「変更があったパッケージだけ」をビルドしたい場面が増えます。--filterはその要です。

–filterの基本パターン10連発

# 特定パッケージだけビルド
pnpm turbo build --filter=web

# scopeつきパッケージ
pnpm turbo build --filter=@my-monorepo/ui

# 複数指定(OR)
pnpm turbo build --filter=web --filter=api

# 依存先も含めて(uiが依存しているパッケージ全部)
pnpm turbo build --filter=@my-monorepo/ui...

# 依存元も含めて(uiに依存しているアプリ全部)
pnpm turbo build --filter=...@my-monorepo/ui

# 依存元+ui自身
pnpm turbo build --filter=...@my-monorepo/ui...

# 除外
pnpm turbo build --filter=!docs

# パスベース(apps配下のみ)
pnpm turbo build --filter='./apps/*'

# git変更があったパッケージだけ
pnpm turbo build --filter=...[HEAD^]

# 特定ブランチからの差分
pnpm turbo build --filter=...[origin/main]

並列度の制御:–concurrency

# 並列度を明示(デフォルトはCPU数)
pnpm turbo build --concurrency=4

# 完全直列(デバッグ用)
pnpm turbo build --concurrency=1

# 並列度をパーセント指定
pnpm turbo build --concurrency=50%

topological(依存順序)実行の仕組み

Turborepoは「依存グラフを解析し、依存先を先にビルドする」を自動で行います。--dry-runで実行順を可視化できます。

# 実行計画を確認(実行はしない)
pnpm turbo build --dry-run

# JSON形式で取得
pnpm turbo build --dry-run=json > build-plan.json

# 依存グラフを画像化(graphvizが必要)
pnpm turbo build --graph=graph.png

UI mode(TUI)— Turborepo 2.xの目玉機能

# インタラクティブなターミナルUIを起動
pnpm turbo dev --ui=tui

# 個別タスクのログに切り替え可能(矢印キーで選択)
# - Web Server
# - Docs Server
# - API Server

従来は複数アプリのログが混ざって見づらかったdevモードが、TUIによってタブ切り替えできるようになりました。--ui=streamで従来の挙動にも戻せます。

internal packages — 型と設定を共有する

Monorepoの真価は「設定」と「コード」を共通化できることにあります。型・ESLint・Tailwind・Vitest・Storybookの設定を一元管理しましょう。

tsconfig共有パッケージ

// packages/tsconfig/package.json
{
  "name": "@my-monorepo/tsconfig",
  "version": "0.0.0",
  "private": true,
  "files": ["base.json", "nextjs.json", "react-library.json", "node.json"]
}
// packages/tsconfig/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "incremental": true,
    "forceConsistentCasingInFileNames": true
  }
}
// packages/tsconfig/nextjs.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "ES2022"],
    "jsx": "preserve",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowJs": true,
    "noEmit": true,
    "plugins": [{ "name": "next" }]
  }
}
// apps/web/tsconfig.json
{
  "extends": "@my-monorepo/tsconfig/nextjs.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

ESLint共有設定(Flat Config時代)

// packages/eslint-config/base.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import prettier from 'eslint-config-prettier'

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  prettier,
  {
    rules: {
      'no-console': ['warn', { allow: ['warn', 'error'] }],
      '@typescript-eslint/consistent-type-imports': 'error',
    },
  },
  {
    ignores: ['dist/**', '.next/**', 'node_modules/**'],
  },
)
// packages/eslint-config/next.js
import base from './base.js'
import nextPlugin from '@next/eslint-plugin-next'
import reactPlugin from 'eslint-plugin-react'
import reactHooksPlugin from 'eslint-plugin-react-hooks'

export default [
  ...base,
  {
    plugins: { '@next/next': nextPlugin, react: reactPlugin, 'react-hooks': reactHooksPlugin },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
    },
  },
]
// apps/web/eslint.config.js
import nextConfig from '@my-monorepo/eslint-config/next'
export default nextConfig

Tailwind共有設定

// packages/tailwind-config/tailwind.config.js
import type { Config } from 'tailwindcss'

const config: Omit<Config, 'content'> = {
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eff6ff',
          500: '#3b82f6',
          900: '#1e3a8a',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
    },
  },
  plugins: [],
}
export default config
// apps/web/tailwind.config.js
import sharedConfig from '@my-monorepo/tailwind-config'

/** @type {import('tailwindcss').Config} */
export default {
  ...sharedConfig,
  content: [
    './src/**/*.{ts,tsx}',
    '../../packages/ui/src/**/*.{ts,tsx}',
  ],
}

Vitest共有設定

// packages/vitest-config/base.ts
import { defineConfig } from 'vitest/config'

export const baseConfig = defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['**/*.config.*', '**/node_modules/**', '**/dist/**'],
    },
  },
})
// packages/vitest-config/react.ts
import { mergeConfig, defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { baseConfig } from './base'

export const reactConfig = mergeConfig(
  baseConfig,
  defineConfig({
    plugins: [react()],
    test: {
      environment: 'happy-dom',
      setupFiles: ['./vitest.setup.ts'],
    },
  }),
)
// apps/web/vitest.config.ts
import { reactConfig } from '@my-monorepo/vitest-config/react'
export default reactConfig

Storybook共有設定

// packages/storybook-config/main.ts
import type { StorybookConfig } from '@storybook/nextjs'

export const baseStorybookConfig: Partial<StorybookConfig> = {
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-a11y',
    '@storybook/addon-interactions',
  ],
  framework: { name: '@storybook/nextjs', options: {} },
  docs: { autodocs: 'tag' },
}
// apps/web/.storybook/main.ts
import type { StorybookConfig } from '@storybook/nextjs'
import { baseStorybookConfig } from '@my-monorepo/storybook-config/main'

const config: StorybookConfig = {
  ...baseStorybookConfig,
  stories: ['../src/**/*.stories.@(ts|tsx)'],
}
export default config

shared/ui パッケージ — コンポーネントを共有する

2つ以上のアプリで同じButton/Cardを使うなら、すぐにpackages/uiに切り出すべきです。重要なのは「ビルドせずにソースのまま参照」する方針です。

packages/ui の構造

packages/ui/
├── src/
│   ├── index.ts
│   ├── button.tsx
│   ├── card.tsx
│   └── input.tsx
├── package.json
├── tsconfig.json
└── eslint.config.js

exports field で複数エントリーポイント

// packages/ui/package.json
{
  "name": "@my-monorepo/ui",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "sideEffects": false,
  "exports": {
    "./button": "./src/button.tsx",
    "./card": "./src/card.tsx",
    "./input": "./src/input.tsx",
    "./styles.css": "./src/styles.css"
  },
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0",
    "react-dom": "^18.0.0 || ^19.0.0"
  },
  "devDependencies": {
    "@my-monorepo/eslint-config": "workspace:*",
    "@my-monorepo/tsconfig": "workspace:*",
    "@types/react": "^19.0.0",
    "react": "^19.0.0",
    "typescript": "^5.5.0"
  }
}

Button コンポーネント本体

// packages/ui/src/button.tsx
import { forwardRef, type ComponentPropsWithoutRef } from 'react'

type Variant = 'primary' | 'secondary' | 'danger'
type Size = 'sm' | 'md' | 'lg'

export interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
  variant?: Variant
  size?: Size
  isLoading?: boolean
}

const variants: Record<Variant, string> = {
  primary: 'bg-brand-500 hover:bg-brand-600 text-white',
  secondary: 'bg-gray-100 hover:bg-gray-200 text-gray-900',
  danger: 'bg-red-500 hover:bg-red-600 text-white',
}
const sizes: Record<Size, string> = {
  sm: 'px-2 py-1 text-sm',
  md: 'px-4 py-2',
  lg: 'px-6 py-3 text-lg',
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', isLoading, children, className = '', ...rest }, ref) => (
    <button
      ref={ref}
      className={`rounded ${variants[variant]} ${sizes[size]} ${className}`}
      disabled={isLoading || rest.disabled}
      {...rest}
    >
      {isLoading ? '...' : children}
    </button>
  ),
)
Button.displayName = 'Button'

Next.js側でtranspilePackages

「ソースのまま参照」方式の場合、Next.jsには明示的に「このパッケージはTranspileしてね」と伝える必要があります。

// apps/web/next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  transpilePackages: [
    '@my-monorepo/ui',
    '@my-monorepo/utils',
  ],
  experimental: {
    typedRoutes: true,
  },
}
export default nextConfig

使う側のコード

// apps/web/src/app/page.tsx
import { Button } from '@my-monorepo/ui/button'

export default function Home() {
  return (
    <main className="p-8">
      <h1 className="text-2xl font-bold">Welcome</h1>
      <Button variant="primary" size="lg" onClick={() => alert('hi')}>
        Click me
      </Button>
    </main>
  )
}

shared/utils パッケージ — ロジックを共通化する

日付処理・バリデーション・API clientなど、フロントエンドとバックエンドで共有したいロジックはpackages/utilsに集めましょう。

packages/utils の構造

packages/utils/
├── src/
│   ├── index.ts
│   ├── date.ts
│   ├── validation.ts
│   ├── api-client.ts
│   └── env.ts
├── package.json
└── tsconfig.json

環境変数のスキーマ検証(Zod)

// packages/utils/src/env.ts
import { z } from 'zod'

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  NEXT_PUBLIC_API_URL: z.string().url(),
  NODE_ENV: z.enum(['development', 'test', 'production']),
})

export type Env = z.infer<typeof envSchema>

export function validateEnv(env: NodeJS.ProcessEnv = process.env): Env {
  const parsed = envSchema.safeParse(env)
  if (!parsed.success) {
    console.error('❌ Invalid env:', parsed.error.flatten().fieldErrors)
    throw new Error('Invalid environment variables')
  }
  return parsed.data
}

共通APIクライアント

// packages/utils/src/api-client.ts
export class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message)
    this.name = 'ApiError'
  }
}

export interface ApiClientOptions {
  baseUrl: string
  headers?: Record<string, string>
}

export function createApiClient({ baseUrl, headers = {} }: ApiClientOptions) {
  async function request<T>(path: string, init?: RequestInit): Promise<T> {
    const res = await fetch(`${baseUrl}${path}`, {
      ...init,
      headers: { 'Content-Type': 'application/json', ...headers, ...init?.headers },
    })
    if (!res.ok) throw new ApiError(res.status, await res.text())
    return res.json() as Promise<T>
  }
  return {
    get: <T>(path: string) => request<T>(path),
    post: <T>(path: string, body: unknown) =>
      request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
  }
}

Next.js × Turborepo 実戦パターン

本記事の主要ターゲットであるNext.js連携を詳しく見ていきます。「2つのNext.jsアプリ + 共通UI」が最頻出構成です。

apps/web(メインサイト)のpackage.json

{
  "name": "web",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev --port 3000",
    "build": "next build",
    "start": "next start",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@my-monorepo/ui": "workspace:*",
    "@my-monorepo/utils": "workspace:*",
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

apps/docs(ドキュメント)のpackage.json

{
  "name": "docs",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev --port 3001",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "@my-monorepo/ui": "workspace:*",
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

両方を同時起動する

# TUIモードで2アプリ同時起動(タブで切り替え)
pnpm turbo dev --ui=tui

# 個別起動も可能
pnpm turbo dev --filter=web
pnpm turbo dev --filter=docs

Next.js App RouterからUIコンポーネントをImport

// apps/web/src/app/products/page.tsx
import { Button } from '@my-monorepo/ui/button'
import { Card } from '@my-monorepo/ui/card'
import { createApiClient } from '@my-monorepo/utils'

const api = createApiClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! })

export default async function ProductsPage() {
  const products = await api.get<Product[]>('/products')
  return (
    <div className="grid grid-cols-3 gap-4 p-8">
      {products.map(p => (
        <Card key={p.id}>
          <h2>{p.name}</h2>
          <Button>Buy</Button>
        </Card>
      ))}
    </div>
  )
}

interface Product { id: string; name: string; price: number }

Remix × Turborepo

Next.js以外のフレームワークもTurborepoに乗せやすい設計になっています。Remixでの実例を見ましょう。

Remixアプリの追加

cd apps
pnpm dlx create-remix@latest admin
# - JavaScript or TypeScript? → TypeScript
# - Initialize a new git repository? → No

共通UIをRemixで使う

// apps/admin/app/routes/_index.tsx
import { Button } from '@my-monorepo/ui/button'
import { Card } from '@my-monorepo/ui/card'

export default function Index() {
  return (
    <div className="p-8">
      <Card>
        <h1>Admin Dashboard</h1>
        <Button variant="danger">Delete</Button>
      </Card>
    </div>
  )
}

RemixのserverDependenciesToBundle設定

// apps/admin/remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
export default {
  serverDependenciesToBundle: [
    /^@my-monorepo/.*/,
  ],
  tailwind: true,
}

Express × Turborepo — APIサーバーを共存させる

フロント+バックエンドを1リポジトリで開発するチームには必須の構成です。型を共有できるのが最大のメリットです。

apps/api(Express)の初期化

mkdir -p apps/api/src
cd apps/api
pnpm init
// apps/api/package.json
{
  "name": "api",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc --build",
    "start": "node dist/index.js",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@my-monorepo/utils": "workspace:*",
    "express": "^4.21.0",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.0",
    "tsx": "^4.19.0",
    "typescript": "^5.5.0"
  }
}

Expressコード本体

// apps/api/src/index.ts
import express from 'express'
import { z } from 'zod'
import { validateEnv } from '@my-monorepo/utils'

const env = validateEnv()
const app = express()
app.use(express.json())

const ProductSchema = z.object({
  id: z.string(),
  name: z.string().min(1),
  price: z.number().positive(),
})

const products: z.infer<typeof ProductSchema>[] = []

app.get('/products', (_req, res) => res.json(products))

app.post('/products', (req, res) => {
  const parsed = ProductSchema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json(parsed.error.flatten())
  products.push(parsed.data)
  res.status(201).json(parsed.data)
})

app.listen(4000, () => console.log(`API listening on :4000 (env=${env.NODE_ENV})`))

型を共有する:packages/types

// packages/types/src/index.ts
import { z } from 'zod'

export const ProductSchema = z.object({
  id: z.string(),
  name: z.string().min(1),
  price: z.number().positive(),
})
export type Product = z.infer<typeof ProductSchema>

export const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
})
export type User = z.infer<typeof UserSchema>

これでapps/apiのサーバー側とapps/webのクライアント側で、1つのZodスキーマから型と実行時バリデーションを両方とも得ることができます。

CI/CD — GitHub Actionsでキャッシュを最大化する

Turborepoの真価はCIで発揮されます。Remote Cacheと組み合わせれば、変更がないジョブは数秒で完了します。

GitHub Actions基本ワークフロー

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

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

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

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

      - run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm turbo lint --filter=...[origin/main]

      - name: Typecheck
        run: pnpm turbo typecheck --filter=...[origin/main]

      - name: Build
        run: pnpm turbo build --filter=...[origin/main]

      - name: Test
        run: pnpm turbo test --filter=...[origin/main]

差分ビルド:変更があったアプリだけビルド

--filter=...[origin/main]がポイントです。「origin/mainから変更があったパッケージ + それに依存する全アプリ」だけを実行します。

# ローカルで動作確認
pnpm turbo build --filter=...[origin/main] --dry-run

# 例:packages/uiだけ変更した場合
# → packages/ui、apps/web、apps/docs、apps/admin が対象
# → apps/api は対象外(uiを使っていないため)

matrixでパッケージごとに並列ジョブ

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        package: [web, docs, admin, api, '@my-monorepo/ui']
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo test --filter=${{ matrix.package }}

turboのキャッシュをGitHub Actions Cacheで保存

      - name: Cache turbo build setup
        uses: actions/cache@v4
        with:
          path: .turbo
          key: ${{ runner.os }}-turbo-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-turbo-

Vercelデプロイ — Turborepo公式の最短ルート

Vercelは「Turborepoの実家」なので、最も摩擦のないデプロイ先です。

各アプリを個別Projectで設定

# apps/web をVercelにデプロイ
cd apps/web
vercel link

# Root Directoryを apps/web に指定
# Build Command: cd ../.. && pnpm turbo build --filter=web...
# Output Directory: .next
# Install Command: cd ../.. && pnpm install --frozen-lockfile

vercel.json で設定を固定

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "buildCommand": "cd ../.. && pnpm turbo build --filter=web...",
  "installCommand": "cd ../.. && pnpm install --frozen-lockfile",
  "ignoreCommand": "cd ../.. && pnpm turbo-ignore web"
}

turbo-ignore で不要なデプロイをスキップ

# webに関係ない変更ならビルドをスキップ
# (例:docsだけ変更されたPushでwebをデプロイしない)
pnpm turbo-ignore web

VercelはignoreCommandが終了コード0を返すとビルドをスキップします。turbo-ignoreはこれを自動判定してくれる便利ツールです。

Cloudflare Pages / Workersへのデプロイ

Vercel以外も問題なく動きます。Cloudflareへのデプロイ手順を見ましょう。

Cloudflare Pages(Next.js)

# @cloudflare/next-on-pagesを使う
pnpm add -D @cloudflare/next-on-pages --filter=web

# ビルドコマンド
pnpm turbo build --filter=web && pnpm dlx @cloudflare/next-on-pages
# wrangler.toml
name = "my-web"
compatibility_date = "2025-01-01"
pages_build_output_dir = ".vercel/output/static"

[vars]
NEXT_PUBLIC_API_URL = "https://api.example.com"

Cloudflare Workers(Hono API)

// apps/api-worker/src/index.ts
import { Hono } from 'hono'
import { ProductSchema } from '@my-monorepo/types'

const app = new Hono()

app.get('/products', c => c.json([{ id: '1', name: 'Test', price: 100 }]))

app.post('/products', async c => {
  const body = await c.req.json()
  const parsed = ProductSchema.safeParse(body)
  if (!parsed.success) return c.json(parsed.error, 400)
  return c.json(parsed.data, 201)
})

export default app

Nx・Lernaとの比較 — どれを選ぶべきか

「Turborepoが一強」ではありません。プロジェクト規模・チーム文化によってベストな選択は変わります。実コードベースで比較します。

機能比較マトリクス

機能 Turborepo 2.x Nx 19+ Lerna 8+
タスクオーケストレーション
Local Cache ×(Nx統合後はNx側)
Remote Cache(無料) ◎(Vercel) △(Nx Cloud有料中心) ×
コードジェネレータ ×(create-turboのみ) ◎(豊富なplugin)
影響範囲検出 ◎(–filter=…[ref]) ◎(nx affected)
publish機能 ×(changeset併用) ◎(本家機能)
学習コスト 中〜高
設定ファイル量 turbo.json 1個 project.json多数 lerna.json 1個

Nxと同じことをTurborepoで書くと?

// Nx: project.json(各パッケージに1個)
{
  "name": "ui",
  "targets": {
    "build": {
      "executor": "@nx/js:swc",
      "outputs": ["{options.outputPath}"],
      "options": { "outputPath": "dist/packages/ui" }
    }
  }
}
// Turborepo: turbo.json 1つ + 各package.jsonにscripts
{
  "tasks": {
    "build": { "outputs": ["dist/**"] }
  }
}
// packages/ui/package.json
{
  "scripts": { "build": "swc src -d dist" }
}

選び方の指針

  • Turborepo: 「ビルドキャッシュとシンプルさが欲しい」「Vercelデプロイがメイン」 → 第一候補
  • Nx: 「Angular中心」「コードジェネレータで標準化したい」「Nx Cloudを既に契約」
  • Lerna(Nx統合版): 「OSSをnpm publishしたい」「version管理を一括でやりたい」

トラブルシューティング — よくある詰まりポイント

1. キャッシュが効かない

# 何がキャッシュキーに含まれているか確認
pnpm turbo build --dry-run=json | jq '.tasks[] | {task, hash, inputs}'

# 環境変数の差で毎回ミスする → globalEnvまたはtask.envに明示
# .envファイルの違いで毎回ミスする → inputsに .env* を含める or 除外

2. transpilePackagesが効かない

// apps/web/next.config.mjs
const nextConfig = {
  transpilePackages: ['@my-monorepo/ui', '@my-monorepo/utils'],
  // ❌ 'ui' のようにscopeなしだとマッチしない
  // ❌ ワイルドカードは未対応(2026年時点)
}

3. 型が解決できない:moduleResolutionの罠

// packages/tsconfig/base.json
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",  // ← これでないとexportsフィールドが効かない
    "verbatimModuleSyntax": true
  }
}

4. pnpm install後にlockfile差分が出る

# CIで「lockfileが古い」エラーになる場合
pnpm install --frozen-lockfile  # CI用
pnpm install --lockfile-only    # lockfileだけ更新

# .npmrcに以下を追加
echo "auto-install-peers=true" >> .npmrc
echo "strict-peer-dependencies=false" >> .npmrc

5. turbo dev で複数アプリのログが混ざる

# TUIモードで切り替え
pnpm turbo dev --ui=tui

# またはログを別ファイルに
pnpm turbo dev --log-prefix=task --log-order=stream

6. Vercelでmonorepoの正しいRoot Directoryが選べない

Vercel Project Settings →
  - Root Directory: apps/web
  - Framework Preset: Next.js
  - Build Command: cd ../.. && pnpm turbo build --filter=web...
  - Install Command: cd ../.. && pnpm install --frozen-lockfile

7. circular dependencyで詰まる

# 循環参照を検出
pnpm dlx madge --circular --extensions ts,tsx packages/ apps/

# turboレベルでも検出される
pnpm turbo build
# Error: detected cycle: ui < utils < ui

8. PNPMのpeer dependencies警告を消す

# pnpm-workspace.yaml に追加(pnpm 9+)
packages:
  - "apps/*"
  - "packages/*"

# .npmrc
auto-install-peers=true
strict-peer-dependencies=false
shamefully-hoist=false

運用Tips — チームで使うための小ワザ集

1. Changeset でversion管理

pnpm add -Dw @changesets/cli
pnpm dlx changeset init

# PRごとに変更内容を記録
pnpm changeset

# versionとchangelog生成
pnpm changeset version

# publish(該当時)
pnpm changeset publish

2. husky + lint-staged でcommit時に検証

pnpm add -Dw husky lint-staged
pnpm dlx husky init
echo "pnpm lint-staged" > .husky/pre-commit
// package.json (root)
{
  "lint-staged": {
    "*.{ts,tsx}": ["pnpm turbo lint --filter=...[HEAD]", "prettier --write"],
    "*.{json,md,yaml}": ["prettier --write"]
  }
}

3. VS Code workspaceの設定

// .vscode/settings.json
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,
  "eslint.workingDirectories": [
    { "pattern": "apps/*" },
    { "pattern": "packages/*" }
  ],
  "search.exclude": {
    "**/node_modules": true,
    "**/.turbo": true,
    "**/.next": true
  }
}

4. turbo runの実行時間を可視化

# Chrome Trace形式で出力
pnpm turbo build --profile=profile.json

# chrome://tracing で profile.json を開く
# → 並列実行・依存待ち時間が視覚化される

学習ロードマップと案件獲得

3週間で習熟するためのプラン

  1. Week 1: create-turboで雛形 → turbo.jsonとpnpm-workspaceの基本を読み解く
  2. Week 2: packages/ui + packages/utils + packages/tsconfig を自前で実装
  3. Week 3: Remote Caching + GitHub Actions + Vercelデプロイまで通す

ポートフォリオに載せるべき成果物

  • 2つのNext.jsアプリ + 共通UI + 共通utilsの完全動作Monorepo
  • GitHub Actionsで差分ビルドが効いているスクリーンショット(time比較)
  • turbo –dry-run=jsonの実行結果・graph画像
  • Remote Cacheが効いた証拠ログ(cache hit, replaying logs (remote))

案件で問われる定番質問

  • 「なぜLernaやNxではなくTurborepoを選んだ?」 → シンプルさ・Remote Cache無料・Vercel連携
  • 「キャッシュキーは何で構成される?」 → inputs + env + turbo.json + 依存hash
  • ^buildbuildの違いは?」 → 依存パッケージ vs 同一パッケージ
  • 「monorepoで型を共有する最小構成は?」 → packages/tsconfig + workspace:* + moduleResolution: bundler
  • 「Vercelで2アプリを別Projectで動かす方法は?」 → Root Directory + Build Commandを各アプリで設定

独学が辛いと感じたら

Turborepoは「動かす」までは独学で行けても、キャッシュキー設計・CI最適化・大規模化に伴うリファクタリングは実務での試行錯誤が必要な領域です。複数アプリ・複数チームの実プロジェクトで揉まれることで初めて勘所が掴めます。スクールでメンターに直接質問できる環境を作ると、学習効率は数倍変わります。

  • テックアカデミー — Next.js / Reactコースで実プロジェクトを伴走指導。週2回のメンタリングでMonorepo設計の判断まで相談できる
  • 侍エンジニア — マンツーマンでTurborepo・モダンフロント全般を学べる。オーダーメイドカリキュラム対応で自社の構成に近づけられる
  • DMM WEBCAMP — 転職保証付き。Next.js + Monorepo構成のモダン現場への転職を目指すなら最有力
  • レバテックフリーランス — 学習後の案件獲得に。Turborepo構成の月単価70万円〜の案件も増加中

まとめ — 2026年のMonorepo戦略

結論を1行で:新規プロジェクト + Next.js/Remix中心 = Turborepo 2.x + pnpm workspace + Vercel Remote Cache がデファクトです。Nxの強力なジェネレータが欲しい、Lernaのpublish機能が必須、というケースを除けば、Turborepoが2026年の最良の選択肢です。

本記事のコードはすべてコピペで動作確認できます。まずはpnpm dlx create-turbo@latestで雛形を作り、packages/uipackages/tsconfigを自作してから、Remote CachingとGitHub Actionsまで一気通貫で組み上げてみてください。一度動かしてしまえば、その後の生産性は劇的に変わります——「変更があったアプリだけ、1分以内にCIが終わる」体験は、一度味わうと戻れなくなります。

関連記事: Next.js 15完全実践ガイド / App Router完全実践ガイド / Remix完全実践ガイド / Vite 6完全実践ガイド / frameworks一覧

コメント

タイトルとURLをコピーしました