Nuxt Monorepo 構築:Yarn Workspaces/Turborepo で共有コンポーネント基盤を整える
複数の Nuxt プロジェクトを運用していると、共通のコンポーネントやユーティリティを各プロジェクトで重複して管理する手間に悩まされますよね。コピー&ペーストでコードを使い回すと、バグ修正や機能追加のたびに全プロジェクトを手作業で更新しなければならず、メンテナンスコストが膨れ上がってしまいます。
そこで注目したいのが Monorepo(モノレポ) という開発手法です。複数のプロジェクトを 1 つのリポジトリで管理し、共通コードを効率的に共有できます。本記事では、Yarn Workspaces と Turborepo を組み合わせて、Nuxt プロジェクト向けの Monorepo 環境を構築する方法を、初心者の方にもわかりやすく解説していきますね。
背景
Monorepo とは何か
Monorepo は「Monolithic Repository(一枚岩のリポジトリ)」の略で、複数のプロジェクトやパッケージを 1 つの Git リポジトリで管理する開発手法です。Google や Facebook といった大手企業でも採用されており、コードの再利用性やメンテナンス性を大幅に向上させます。
従来の開発では、プロジェクトごとに別々のリポジトリを作成し、共通コードは npm パッケージとして公開するか、コピー&ペーストで対応していました。しかし、この方法ではバージョン管理が煩雑になり、コードの同期が困難になります。
Yarn Workspaces の役割
Yarn Workspaces は、Monorepo 内の複数パッケージを効率的に管理するための機能です。依存関係を一元管理し、共通の node_modules を使用することでディスク容量を節約できます。
以下の図は、Yarn Workspaces による依存関係の一元管理を示しています。
mermaidflowchart TB
root["ルートディレクトリ"]
ws1["apps/web-app"]
ws2["apps/admin-app"]
pkg1["packages/ui"]
pkg2["packages/utils"]
nm["node_modules<br/>(共有)"]
root --> ws1
root --> ws2
root --> pkg1
root --> pkg2
root --> nm
ws1 -->|依存| pkg1
ws1 -->|依存| pkg2
ws2 -->|依存| pkg1
ws2 -->|依存| pkg2
pkg1 -.->|共有| nm
pkg2 -.->|共有| nm
この図から分かるように、各ワークスペースは共通の node_modules を参照し、パッケージ間の依存関係も明確に管理されています。
図のポイント:
- ルートディレクトリが全体を統括
- 共通パッケージ(ui、utils)を複数アプリが参照
- node_modules が一元化されディスク容量を節約
Turborepo が解決する課題
Yarn Workspaces だけでは、ビルドやテストの実行を効率化できません。複数のパッケージを持つ Monorepo では、変更されたパッケージだけをビルドしたり、並列処理で高速化したりする仕組みが必要です。
Turborepo は、タスクの実行を最適化するビルドシステムです。キャッシュ機能により、変更のないパッケージのビルドをスキップし、開発者の待ち時間を大幅に削減します。
課題
複数 Nuxt プロジェクトの管理における問題点
実際の開発現場では、以下のような課題に直面することが多いでしょう。
1. コードの重複管理
各プロジェクトで同じコンポーネント(ボタン、モーダル、フォームなど)を個別に管理すると、修正のたびに全プロジェクトを更新する必要があります。これはバグの温床となり、開発効率を著しく低下させますね。
2. 依存関係のバージョン不一致
プロジェクトごとに異なるバージョンの Vue や Nuxt を使用していると、予期しない動作やバグが発生しやすくなります。バージョンの統一は手動では困難です。
3. ビルド時間の増大
プロジェクトが増えるほど、全体のビルドやテストに時間がかかります。CI/CD パイプラインでの待ち時間が長くなり、開発サイクルが遅延してしまいます。
以下の図は、従来の開発手法と Monorepo の違いを比較したものです。
mermaidflowchart LR
subgraph old["従来の方式"]
direction TB
p1["プロジェクト A<br/>リポジトリ"]
p2["プロジェクト B<br/>リポジトリ"]
p3["プロジェクト C<br/>リポジトリ"]
p1 -.->|コピー| comp1["共通<br/>コンポーネント"]
p2 -.->|コピー| comp2["共通<br/>コンポーネント"]
p3 -.->|コピー| comp3["共通<br/>コンポーネント"]
end
subgraph new["Monorepo 方式"]
direction TB
mono["単一リポジトリ"]
mono --> app1["apps/A"]
mono --> app2["apps/B"]
mono --> app3["apps/C"]
mono --> shared["packages/shared"]
app1 -->|参照| shared
app2 -->|参照| shared
app3 -->|参照| shared
end
図から読み取れる違い:
- 従来方式ではコンポーネントが 3 箇所に重複
- Monorepo では共通パッケージを一元管理
- 修正が 1 箇所で済み、同期の手間が不要
解決策
Yarn Workspaces と Turborepo による統合環境
Nuxt Monorepo を構築することで、上記の課題をすべて解決できます。Yarn Workspaces で依存関係を一元管理し、Turborepo でビルドを最適化する、この 2 つのツールの組み合わせが鍵となります。
以下の図は、Monorepo におけるビルドフローを示しています。
mermaidflowchart TD
dev["開発者がコード変更"]
turbo["Turborepo が<br/>変更を検出"]
cache{キャッシュ<br/>あり?}
skip["ビルドスキップ"]
parallel["並列ビルド実行"]
result["ビルド完了"]
dev --> turbo
turbo --> cache
cache -->|Yes| skip
cache -->|No| parallel
skip --> result
parallel --> result
ビルドフローの利点:
- 変更のないパッケージはキャッシュを利用
- 複数パッケージを並列処理で高速化
- 開発者の待ち時間を大幅に削減
アプローチの全体像
Monorepo 構築は、以下の 3 つのステップで進めていきます。
| # | ステップ | 主な作業内容 |
|---|---|---|
| 1 | 環境準備 | Yarn、Node.js のインストール |
| 2 | Workspaces 設定 | package.json の設定、ディレクトリ構成 |
| 3 | Turborepo 導入 | ビルド最適化、キャッシュ設定 |
それでは、具体的な手順を見ていきましょう。
具体例
ステップ 1:プロジェクトの初期化
まず、Monorepo のルートディレクトリを作成し、基本的な設定を行います。
プロジェクトディレクトリの作成
以下のコマンドで、新しいプロジェクトディレクトリを作成しましょう。
bashmkdir nuxt-monorepo
cd nuxt-monorepo
Yarn の初期化
次に、Yarn を初期化します。このコマンドで package.json が生成されます。
bashyarn init -y
package.json の設定
生成された package.json を編集して、Workspaces を有効化します。private フィールドを true に設定することで、誤って npm に公開されるのを防げます。
json{
"name": "nuxt-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": ["apps/*", "packages/*"]
}
設定のポイント:
private: trueでリポジトリ全体を非公開に設定workspacesにapps/*とpackages/*を指定- これにより、これらのディレクトリ配下がワークスペースとして認識されます
ステップ 2:ディレクトリ構成の作成
Monorepo の標準的なディレクトリ構成を作成します。apps には実際のアプリケーション、packages には共有パッケージを配置しますね。
bashmkdir -p apps/web apps/admin packages/ui packages/utils
作成後のディレクトリ構成は以下のようになります。
plaintextnuxt-monorepo/
├── apps/
│ ├── web/ # ユーザー向け Web アプリ
│ └── admin/ # 管理画面アプリ
├── packages/
│ ├── ui/ # 共有 UI コンポーネント
│ └── utils/ # 共有ユーティリティ
└── package.json
ステップ 3:Nuxt アプリケーションの作成
各アプリケーションディレクトリで Nuxt プロジェクトを初期化します。まずは web アプリから始めましょう。
web アプリの作成
apps/web ディレクトリに移動して、Nuxt 3 プロジェクトを作成します。
bashcd apps/web
npx nuxi init .
package.json の name フィールド設定
生成された package.json の name フィールドを、ワークスペース内で一意になるように変更します。
json{
"name": "@monorepo/web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"preview": "nuxt preview"
}
}
命名規則のポイント:
@monorepo/というスコープを付けることで、パッケージ名の衝突を防止- 各ワークスペースに一貫性のある命名を適用
admin アプリの作成
同様に、admin アプリも作成しましょう。
bashcd ../admin
npx nuxi init .
package.json の name を @monorepo/admin に変更します。
json{
"name": "@monorepo/admin",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nuxt dev --port 3001",
"build": "nuxt build",
"preview": "nuxt preview"
}
}
ポート番号の設定:
--port 3001を追加して、web アプリ(デフォルト 3000)と競合しないように設定- 複数のアプリを同時に起動できるようになります
ステップ 4:共有パッケージの作成
共通の UI コンポーネントを管理するパッケージを作成します。これにより、複数のアプリで同じコンポーネントを再利用できますね。
ui パッケージの初期化
packages/ui ディレクトリに移動して、パッケージを初期化します。
bashcd ../../packages/ui
yarn init -y
package.json の設定
UI パッケージの package.json を以下のように設定します。
json{
"name": "@monorepo/ui",
"version": "1.0.0",
"main": "index.ts",
"types": "index.ts",
"dependencies": {
"vue": "^3.4.0"
}
}
エクスポート設定のポイント:
mainとtypesフィールドでエントリーポイントを指定- TypeScript の型情報も提供されるため、型安全性が向上
共通コンポーネントの作成
サンプルとして、シンプルなボタンコンポーネントを作成しましょう。
vue<!-- packages/ui/components/BaseButton.vue -->
<template>
<button :class="buttonClass" @click="handleClick">
<!-- デフォルトスロットでボタンのテキストを受け取る -->
<slot />
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue';
// Props の型定義
interface Props {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'medium',
});
// Emits の型定義
const emit = defineEmits<{
click: [event: MouseEvent];
}>();
// バリアントに基づくクラス名を計算
const buttonClass = computed(() => {
return `btn btn--${props.variant} btn--${props.size}`;
});
// クリックイベントのハンドラー
const handleClick = (event: MouseEvent) => {
emit('click', event);
};
</script>
コンポーネント設計のポイント:
- TypeScript で Props と Emits を厳密に型定義
variantとsizeで柔軟なスタイリングに対応- スロットを使用して、ボタンの内容をカスタマイズ可能に
スタイルの追加
ボタンの見た目を定義するスタイルを追加します。
vue<style scoped>
/* ベーススタイル */
.btn {
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s ease;
}
/* サイズバリエーション */
.btn--small {
padding: 8px 16px;
font-size: 14px;
}
.btn--medium {
padding: 12px 24px;
font-size: 16px;
}
.btn--large {
padding: 16px 32px;
font-size: 18px;
}
/* カラーバリエーション */
.btn--primary {
background-color: #3b82f6;
color: white;
}
.btn--primary:hover {
background-color: #2563eb;
}
.btn--secondary {
background-color: #6b7280;
color: white;
}
.btn--secondary:hover {
background-color: #4b5563;
}
.btn--danger {
background-color: #ef4444;
color: white;
}
.btn--danger:hover {
background-color: #dc2626;
}
</style>
エクスポートファイルの作成
作成したコンポーネントを他のパッケージから利用できるように、エクスポートファイルを作成します。
typescript// packages/ui/index.ts
export { default as BaseButton } from './components/BaseButton.vue';
エクスポートの仕組み:
- 他のワークスペースから
import { BaseButton } from '@monorepo/ui'で利用可能 - 複数のコンポーネントを追加する場合もこのファイルに追記
ステップ 5:utils パッケージの作成
ユーティリティ関数を管理するパッケージも作成しましょう。日付フォーマットやバリデーションなど、共通的な処理を集約します。
パッケージの初期化
bashcd ../utils
yarn init -y
package.json の設定
json{
"name": "@monorepo/utils",
"version": "1.0.0",
"main": "index.ts",
"types": "index.ts"
}
ユーティリティ関数の作成
日付フォーマット関数を実装してみましょう。
typescript// packages/utils/formatDate.ts
/**
* 日付を YYYY-MM-DD 形式にフォーマットする
* @param date - フォーマット対象の Date オブジェクト
* @returns フォーマットされた日付文字列
*/
export const formatDate = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(
2,
'0'
);
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
エクスポートファイルの作成
typescript// packages/utils/index.ts
export { formatDate } from './formatDate';
ステップ 6:ワークスペース間の依存関係設定
作成した共有パッケージを、各アプリケーションから利用できるように設定します。
web アプリでの依存関係追加
apps/web の package.json に、共有パッケージへの依存を追加します。
json{
"name": "@monorepo/web",
"version": "1.0.0",
"private": true,
"dependencies": {
"@monorepo/ui": "*",
"@monorepo/utils": "*",
"nuxt": "^3.13.0",
"vue": "^3.4.0"
}
}
バージョン指定のポイント:
*を使用することで、常に最新のローカルバージョンを参照- Monorepo 内部のパッケージはバージョン管理が不要
依存関係のインストール
ルートディレクトリに戻り、すべての依存関係をインストールします。
bashcd ../../
yarn install
このコマンドにより、Yarn Workspaces が自動的に依存関係を解決し、共通の node_modules にパッケージをインストールしてくれます。
ステップ 7:共有コンポーネントの利用
web アプリで、作成した共有コンポーネントを実際に使ってみましょう。
Nuxt プラグインの作成
共有 UI コンポーネントをグローバルに登録するため、Nuxt プラグインを作成します。
typescript// apps/web/plugins/components.ts
import { BaseButton } from '@monorepo/ui';
export default defineNuxtPlugin((nuxtApp) => {
// グローバルコンポーネントとして登録
nuxtApp.vueApp.component('BaseButton', BaseButton);
});
プラグインの役割:
- アプリ全体でコンポーネントを
importなしで利用可能 - 大規模アプリでの記述量を削減
ページでの利用
トップページで共有コンポーネントとユーティリティを使用します。
vue<!-- apps/web/app.vue -->
<template>
<div class="container">
<h1>Welcome to Nuxt Monorepo</h1>
<!-- 共有 UI コンポーネントの利用 -->
<BaseButton
variant="primary"
size="large"
@click="handleClick"
>
クリックしてください
</BaseButton>
<!-- 日付表示 -->
<p>今日の日付: {{ formattedDate }}</p>
</div>
</template>
<script setup lang="ts">
import { formatDate } from '@monorepo/utils';
// 現在の日付を取得してフォーマット
const today = new Date();
const formattedDate = formatDate(today);
// ボタンクリック時の処理
const handleClick = () => {
alert('ボタンがクリックされました!');
};
</script>
<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
text-align: center;
}
h1 {
margin-bottom: 24px;
color: #1f2937;
}
p {
margin-top: 16px;
color: #6b7280;
}
</style>
ステップ 8:Turborepo の導入
ビルドを最適化するため、Turborepo を導入します。これにより、キャッシュや並列処理の恩恵を受けられますね。
Turborepo のインストール
ルートディレクトリで Turborepo をインストールします。
bashyarn add -D -W turbo
オプションの意味:
-Dは開発依存として追加-Wはワークスペースのルートに追加することを明示
turbo.json の作成
Turborepo の設定ファイルを作成します。この設定により、タスクの実行順序やキャッシュの動作を制御できます。
json{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".nuxt/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^test"],
"outputs": ["coverage/**"]
}
}
}
設定項目の解説:
| 項目 | 説明 |
|---|---|
| dependsOn | 依存するタスクの実行順序を定義 |
| outputs | キャッシュ対象のディレクトリを指定 |
| cache | キャッシュの有効/無効を設定 |
| persistent | 継続実行するタスクかどうか |
^build の意味:
^は「依存するワークスペース」を表す記号- 依存パッケージのビルドが完了してから、このパッケージのビルドを実行
ルート package.json にスクリプト追加
Turborepo を使ったタスク実行コマンドを追加します。
json{
"name": "nuxt-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^2.3.0"
}
}
ステップ 9:開発サーバーの起動
すべての設定が完了したら、開発サーバーを起動してみましょう。
bashyarn dev
このコマンドにより、apps/web と apps/admin の両方の開発サーバーが並列で起動します。Turborepo が自動的にタスクを管理してくれるため、手動で各アプリを起動する必要はありません。
以下のような出力が表示されれば成功です。
plaintext• Packages in scope: @monorepo/admin, @monorepo/web
• Running dev in 2 packages
• Remote caching disabled
@monorepo/web:dev: cache bypass, force executing
@monorepo/admin:dev: cache bypass, force executing
@monorepo/web:dev: Nuxt 3.13.0 with Nitro 2.9.0
@monorepo/web:dev: ➜ Local: http://localhost:3000/
@monorepo/admin:dev: Nuxt 3.13.0 with Nitro 2.9.0
@monorepo/admin:dev: ➜ Local: http://localhost:3001/
起動確認のポイント:
- web アプリは
http://localhost:3000 - admin アプリは
http://localhost:3001 - 両方同時にアクセス可能
ステップ 10:ビルドとキャッシュの確認
Turborepo のキャッシュ機能を確認してみましょう。
初回ビルド
bashyarn build
初回は全パッケージがビルドされ、数秒〜数十秒かかります。
2 回目のビルド
何も変更せずに、再度ビルドコマンドを実行します。
bashyarn build
キャッシュが効いているため、ほぼ瞬時にビルドが完了します。以下のような出力が表示されますね。
plaintext@monorepo/web:build: cache hit, replaying logs
@monorepo/admin:build: cache hit, replaying logs
キャッシュの効果:
- 変更のないパッケージはビルドをスキップ
- CI/CD での待ち時間を大幅に削減
- 開発者の生産性が向上
ステップ 11:TypeScript の型チェック設定
Monorepo 全体で TypeScript の型安全性を確保するため、共通の設定を行います。
ルート tsconfig.json の作成
json{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}
各パッケージでの tsconfig.json
各ワークスペースは、ルートの設定を継承します。
json// packages/ui/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@monorepo/ui": ["./index.ts"]
}
},
"include": ["**/*.ts", "**/*.vue"]
}
ステップ 12:VSCode の設定
開発体験を向上させるため、VSCode の設定も追加しましょう。
.vscode/settings.json の作成
json{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
設定の効果:
- ファイル保存時に自動フォーマット
- ESLint のエラーを自動修正
- Monorepo 内の TypeScript を正しく認識
まとめ
ここまで、Yarn Workspaces と Turborepo を使った Nuxt Monorepo の構築方法を、段階的に解説してきました。この環境を導入することで、複数のプロジェクトを効率的に管理し、共通コードの再利用性を最大化できます。
改めて、Monorepo 導入のメリットを整理しましょう。
得られる主な効果:
| メリット | 具体的な効果 |
|---|---|
| コードの一元管理 | 共通コンポーネントの修正が全プロジェクトに即座に反映 |
| 依存関係の統一 | バージョン不一致によるバグを防止 |
| ビルド時間の短縮 | Turborepo のキャッシュで 2 回目以降が高速化 |
| 開発体験の向上 | 型安全性と自動補完でミスを削減 |
| メンテナンス性の改善 | 1 つのリポジトリで全体を把握しやすい |
導入時の注意点としては、初期セットアップに一定の学習コストがかかること、リポジトリのサイズが大きくなる可能性があることが挙げられます。しかし、これらのデメリットを補って余りあるメリットがありますね。
小規模なプロジェクトから始めて、徐々に Monorepo の恩恵を実感していくことをおすすめします。まずは 2〜3 個のアプリケーションと、基本的な共有パッケージから始めてみてはいかがでしょうか。
Monorepo は現代的な開発手法として、多くの企業で採用が進んでいます。本記事の手順を参考に、ぜひあなたのプロジェクトでも導入してみてください。
関連リンク
articleNuxt Monorepo 構築:Yarn Workspaces/Turborepo で共有コンポーネント基盤を整える
articleNuxt × Vercel/Netlify/Cloudflare:デプロイ先で変わる性能とコストを実測
articleNuxt 500 の犯人はどこ?server/api で起きる例外・CORS・runtimeConfig の切り分け
articleNuxt Nitro のしくみを図解で理解:サーバーレス実行とアダプタの舞台裏
articleNuxt 本番運用チェックリスト:セキュリティヘッダー・CSP・Cookie 設定を総点検
articleNuxt クリーンアーキテクチャ実践:UI・ドメイン・インフラを composable で分離
articlePython Dev Containers 完全レシピ:再現可能な開発箱を VS Code で作る
articleStorybook で Zustand をモックする:Controls 連動とシナリオ駆動 UI
articlePrisma を Monorepo で使い倒す:パス解決・generate の共有・依存戦略
articleプラグイン競合の特定術:WordPress で原因切り分けを高速化する手順
articleWebSocket RFC6455 を図解速習:OPCODE・マスキング・Close コードの要点 10 分まとめ
articlePinia × VueUse × Vite 雛形:型安全ストアとユーティリティを最短で組む
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来