🧩 はじめに
複数のアプリケーション(API / Web)と、共通ロジック(DB / UI / 型 / 設定)を
1つのリポジトリで管理したい場合、Monorepoは非常に有効な選択肢です。
しかし、設計を誤ると以下のような問題が発生します:
- ❌ ビルドが遅い
- ❌ DTOが重複する(Backend / Frontend)
- ❌ Prisma schemaが分散する
- ❌
../../packages/...のimport地獄 - ❌ スケールしない構成
🎯 本記事のゴール
以下の構成で、実務レベルのMonorepo設計を理解します:
- Turborepo
- pnpm workspace
- NestJS(Backend)
- Next.js(Frontend)
- Prisma + PostgreSQL
🏗️ 全体構成
apps/
api/ # NestJS(Backend)
web/ # Next.js(Frontend)
packages/
api/ # API contract(DTO / 型)
database/ # Prisma(schema / client)
ui/ # UIコンポーネント
config/ # 共通設定(eslint / tsconfig)
📦 pnpm workspace
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
👉 apps / packages を明確に分離することで、責務をコントロールできる
🔥 最重要:Boundary(境界設計)
Monorepoで最も重要なのは ツールではなく設計 です。
🧱 レイヤー定義
| レイヤー | 役割 |
|---|---|
| apps | 実行される場所(runtime) |
| packages | 再利用可能なロジック |
📌 apps の責務
- Controller
- Routing
- DI(Dependency Injection)
- Server起動
📌 packages の責務
- 型(DTO)
- DB client
- UI
- 共通設定
⚠️ 境界が崩れると…
- DTOが重複する
- Prisma schemaが複数存在する
- importが複雑になる
- 保守性が崩壊する
👉 Monorepoの成否はBoundaryで決まる
🔗 workspace: による内部依存
🧩 apps/api(NestJS)
"dependencies": {
"@repo/api": "workspace:*",
"@repo/database": "workspace:*"
}
👉 APIは以下に依存:
- API contract(型)
- DB client
🧩 apps/web(Next.js)
"dependencies": {
"@repo/ui": "workspace:*",
"next": "16.x",
"react": "^19.x"
},
"devDependencies": {
"@repo/api": "workspace:*"
}
💡 ポイント
- UI → runtime依存
- API contract → 型のみ(devDependencies)
👉 Frontendは「型だけ欲しい」が多い
📦 @repo/api(API Contract)
🎯 役割
- DTO / Response型を管理
- 実行しない(pure library)
package.json
{
"main": "./dist/entry.js",
"types": "./dist/entry.d.ts",
"exports": {
".": {
"import": "./dist/entry.js"
}
}
}
scripts
{
"scripts": {
"dev": "pnpm build --watch",
"build": "tsc -b"
}
}
👉 libraryとしてビルドするのがポイント
🗄️ @repo/database(Prisma集約)
🎯 ベストプラクティス
👉 DB関連はすべてここに集約
packages/database/
prisma/
schema.prisma
src/
client.ts
seed.ts
scripts
{
"scripts": {
"build": "prisma generate && tsc",
"db:migrate:dev": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy",
"db:push": "prisma db push",
"db:seed": "tsx src/seed.ts",
"generate": "prisma generate"
}
}
🔥 なぜ重要?
- ✅ schemaが一元管理される
- ✅ migrationの衝突を防げる
- ✅ APIはclientを使うだけ
- ✅ チーム開発に強い
⚡ Turborepo Task設計(超重要)
turbo.json
{
"tasks": {
"dev": {
"cache": false,
"persistent": true
},
"build": {
"dependsOn": ["^build", "^generate"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "dist/**"]
},
"db:migrate:dev": { "cache": false },
"db:seed": { "cache": false },
"generate": {
"dependsOn": ["^generate"],
"cache": false
},
"lint": {},
"test": {}
}
}
🔑 ポイント解説
① dependsOn
"dependsOn": ["^build", "^generate"]
👉 依存順で実行:
packages → apps
② cache戦略
| Task | Cache |
|---|---|
| build | ✅ |
| dev | ❌ |
| DB系 | ❌ |
③ outputs
"outputs": [".next/**", "dist/**"]
👉 キャッシュ対象を明確化
④ .env を含める
"inputs": ["$TURBO_DEFAULT$", ".env*"]
👉 環境差によるキャッシュバグ防止
🧪 root scripts
{
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"db:migrate:dev": "turbo run db:migrate:dev",
"db:seed": "turbo run db:seed",
"lint": "turbo run lint",
"test": "turbo run test"
}
}
🔄 運用フロー
🚀 開発
pnpm dev
🏗️ ビルド
pnpm build
🗄️ DB操作
pnpm db:migrate:dev
pnpm db:seed
pnpm db:push
✅ 品質チェック
pnpm lint
pnpm test
💡 この構成の強み
- ✅ DTOの重複を防ぐ →
@repo/api - ✅ Prismaの一元管理 →
@repo/database - ✅ 高速ビルド → Turborepo cache
- ✅ スケーラブル → 明確なBoundary
⚠️ よくあるミス
❌ DBタスクをキャッシュする
"cache": false
👉 データ不整合の原因になる
❌ .envをinputsに含めない
👉 キャッシュバグ発生
❌ @repo/api が DB に依存する
api → database ❌
👉 Boundary崩壊
🧠 まとめ
重要なのは「Turborepoの速さ」ではありません。
🔥 本質
👉 Boundary設計 × Task設計
🧩 最終原則
- apps = runtime
- packages = shared
- dependency = workspace:*
- Prisma = @repo/database
- DTO = @repo/api
- Turbo = build + cache管理