個人開発でTypeScriptモノレポ化すべきか|Turborepo+pnpm workspace最小構成手順2025
結論:パッケージが3つ超えたらモノレポ化を検討する
個人開発でモノレポ化を検討すべきタイミングは明確だ。
- Webアプリ+APIサーバー+共通型定義など、リポジトリが2〜3個に分裂してきた
- 型定義やUIコンポーネントを複数プロジェクトで使い回したい
npm linkや手動コピペで型がズレる事故が起きた
逆に、単一のNext.jsアプリだけで完結しているなら、モノレポ化は不要だ。構成の複雑さが増える分だけ、着手速度は落ちる。
本記事では、個人開発規模を想定して次を扱う。
- モノレポ化の損益分岐点(メリット・デメリットの実態)
- Turborepo + pnpm workspaceの最小構成手順
- よくあるハマりどころと対処
モノレポ化の損益分岐点
メリット
| メリット | 具体例 |
|---|---|
| 型の一元管理 | APIのレスポンス型をフロントとバックで共有できる |
| 変更の一括反映 | 共通UIコンポーネントの修正がアプリ全体に即座に効く |
| CI/デプロイの共通化 | lint・testの設定を1箇所で管理できる |
| ビルドキャッシュ | Turborepoのキャッシュで再ビルド時間を大幅短縮できる |
デメリット
| デメリット | 具体例 |
|---|---|
| 初期構築コストがかかる | workspace設定・tsconfig継承の設計に半日〜1日 |
| CI設定が複雑になる | affected packageだけテストする仕組みが必要になる |
| Gitの差分が読みにくくなる | 複数パッケージの変更が1コミットに混ざりやすい |
| デプロイ先の制約 | Cloudflare PagesやVercelでのmonorepoビルド設定が必要 |
個人開発では「今すぐ複数プロダクトを並行運用している」か「共通ライブラリを外部公開したい」場合を除き、慌てて導入する必要はない。逆に言えば、その条件に当てはまったら早めに移行したほうが後戻りコストは小さい。
最小構成の全体像
今回作る構成は次の通りだ。
my-monorepo/
├── apps/
│ ├── web/ # Next.js などのフロントエンド
│ └── api/ # Honoなどのバックエンド
├── packages/
│ ├── shared-types/ # 共通の型定義
│ └── ui/ # 共通UIコンポーネント
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.base.json
手順1:pnpm workspaceの初期化
pnpmが未導入なら先に入れる。
npm install -g pnpm
mkdir my-monorepo && cd my-monorepo
pnpm init
ワークスペースの範囲をpnpm-workspace.yamlで定義する。
packages:
- "apps/*"
- "packages/*"
手順2:各パッケージを作成する
mkdir -p apps/web apps/api packages/shared-types packages/ui
packages/shared-types/package.jsonの例。
{
"name": "@myorg/shared-types",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts"
}
apps/web側から参照するには、package.jsonのdependenciesに workspace プロトコルで指定する。
{
"dependencies": {
"@myorg/shared-types": "workspace:*"
}
}
pnpm installを実行すると、シンボリックリンクでパッケージ間が接続される。ビルド不要で即座に型変更が反映される点がworkspaceの利点だ。
手順3:Turborepoを導入する
pnpm add -D turbo -w
ルートにturbo.jsonを置く。
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {},
"test": {
"dependsOn": ["^build"]
}
}
}
dependsOn: ["^build"]は「依存パッケージのbuildを先に終わらせる」という意味だ。shared-typesを先にビルドしてからwebをビルドする、という依存順序をここで表現する。
ルートのpackage.jsonにスクリプトを追加する。
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
}
}
pnpm buildを実行すると、依存関係を解決した順にビルドが走り、2回目以降は変更のないパッケージがキャッシュから即復元される。
手順4:tsconfigの共有
ルートにtsconfig.base.jsonを置き、各パッケージからextendsする構成にすると保守が楽になる。
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
各パッケージのtsconfig.json。
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
ハマりどころと対処
workspace依存のバージョン解決でズレる
workspace:*は常に最新のローカルパッケージを指すが、公開時(npm publish)には実バージョンに置換される。個人開発でパッケージを外部公開しないならworkspace:*のままで問題ない。
デプロイ先がmonorepoのビルドコンテキストを認識しない
Cloudflare Pagesなどでデプロイする場合、ビルドコマンドをturbo run build --filter=webのように対象パッケージだけに絞る設定が必要になる。デプロイ設定の細かい挙動は変わりやすいため、公式の最新情報を確認してから設定するのが安全だ。関連する具体手順はCloudflare PagesにAstroサイトをデプロイする方法|手順と詰まりどころも参考になる。
循環依存に気づかない
パッケージ数が増えると、uiがshared-typesを参照し、shared-typesがuiの型を参照するような循環が発生しやすい。Turborepoはこれを検出してエラーにするので、発生したら依存方向を一方向に整理し直す。
CIの実行時間がかえって伸びる
キャッシュが効かない初回実行やリモートキャッシュ未設定の状態では、逆にオーバーヘッドが増えることがある。個人開発でCIを使うなら、Turborepoのリモートキャッシュ機能の設定も合わせて検討するとよい。料金体系は変更されることがあるため、導入前に公式の最新情報を確認してほしい。
まとめ
モノレポ化は「複数パッケージ間で型や処理を共有したくなった瞬間」が導入の適切なタイミングだ。Turborepo + pnpm workspaceの組み合わせは、個人開発規模であれば半日程度でも最小構成を組める。ただし初期設定とデプロイ設定の複雑化というコストは確実に発生するため、単一アプリで完結する間は無理に導入しないほうが開発速度は保てる。まずはpnpm-workspace.yamlとturbo.jsonだけの最小構成から試し、必要に応じてパッケージを切り出していくのが失敗の少ない進め方だ。