記事一覧に戻る
Turborepo + pnpmで作る快適モノレポ開発環境

Turborepo + pnpmで作る快適モノレポ開発環境

TurborepoとpnpmでAPIとフロントエンドを1リポジトリ管理するモノレポ構成の作り方。pnpm workspaceの設定、turbo.jsonの依存関係定義、型共有のメリットを実例で解説。

プロジェクトが大きくなってくると、「これ、どう整理したらいいんだろう」と悩むことがあります。

APIとフロントエンドを同じリポジトリで管理したいけど、依存関係がぐちゃぐちゃになるのは嫌だなと思っていました。そこで試してみたのがTurborepo + pnpmのモノレポ構成です。

なぜモノレポにしたか

正直、最初は「モノレポって大げさじゃない?」と思っていました。

でも実際に使ってみると、地味に便利なポイントがいくつかあって:

  • APIとフロントエンドで型定義を共有できる
  • どちらかを変更したときに、影響範囲がすぐわかる
  • pnpm dev 一発で全部立ち上がる

特に型の共有は大きくて、APIのレスポンス型を変えたらフロントエンドでTypeScriptエラーが出る、という体験は一度味わうと戻れなくなります。

プロジェクト構成

こんな感じの構成にしています:

project/
├── apps/
│   ├── api/          # バックエンドAPI
│   ├── web/          # フロントエンドSPA
│   └── ...           # その他のアプリ
├── packages/
│   ├── domain/       # エンティティ、値オブジェクト
│   ├── usecase/      # ビジネスロジック
│   ├── infrastructure/  # DB実装
│   └── ...           # 共有ライブラリ
└── turbo.json

apps/ にはデプロイ単位のアプリケーション、packages/ には共有ライブラリを置いています。

pnpmのワークスペース設定

pnpm-workspace.yaml はシンプルです:

packages:
  - apps/*
  - apps/workers/*
  - apps/batch/*
  - packages/*

catalog:
  '@hono/zod-openapi': 1.2.0
  drizzle-orm: 0.45.1
  hono: 4.11.1
  zod: 4.2.1

catalog という機能が地味に便利で、複数パッケージで使うライブラリのバージョンを一箇所で管理できます。各 package.json では "zod": "catalog:" と書くだけ。バージョンアップ時に全パッケージを一括で変更しなくて済むのがいい。

Turborepoのタスク設定

turbo.json では依存関係を定義します:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build", "^generate"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true,
      "dependsOn": ["^build"]
    },
    "generate": {
      "dependsOn": ["^generate:openapi"],
      "outputs": ["src/generated/**"]
    }
  }
}

^build^ は「このパッケージが依存しているパッケージのbuildを先に実行する」という意味です。これがあるおかげで、pnpm build と打つだけで正しい順序でビルドが走ります。

依存関係の方向

パッケージ間の依存は一方向に制限しています:

domain ← usecase ← infrastructure/routes ← apps

domain は他のパッケージに依存しない純粋な層。エンティティや値オブジェクトを定義していて、Zodスキーマがそのまま型として使われます。

// packages/domain/src/entities/item.ts
import { z } from "zod";

export const Item = z.object({
  id: ItemId,
  name: ItemName,
  status: ItemStatus,
});
export type Item = z.infer<typeof Item>;

この Item 型がAPIレスポンス、DBモデル、フロントエンドの状態管理まで一貫して使われる。型の変更が自動的に全層に伝播するので、「ここ変えたけど、他にも変えなきゃいけない場所あったっけ?」という心配がなくなります。

開発時のワークフロー

普段の開発はこんな感じ:

# 全サービス起動
pnpm dev

# 特定のパッケージだけ
pnpm --filter @myapp/api dev

Turborepoのキャッシュが効いているので、変更していないパッケージは再ビルドされません。packages/domain を変更したら依存しているパッケージだけ再ビルド、みたいな感じで動いてくれる。

型チェック・Lintの一括実行

CI/CDや手元での確認用に、全パッケージの品質チェックを一括で実行できます:

pnpm type-check  # 全パッケージのtsc
pnpm lint        # 全パッケージのBiome
pnpm test        # 全パッケージのテスト

これもTurborepoの依存関係解決のおかげで、必要なビルドが先に走ってから型チェックが実行されます。

感想

モノレポにしてみて思うのは、「意外とアリ」ということ。

最初はセットアップが面倒かな?と思っていたけど、Turborepo + pnpmの組み合わせはドキュメントがしっかりしていて、短時間で動く状態にできました。

特に型の共有とキャッシュの恩恵は大きくて、「APIの型変えたのにフロントの修正忘れてた」みたいなバグが消えるのは精神的に楽です。