npm workspaces でモノレポのリポジトリを作ってみた

npm workspaces でモノレポのリポジトリを作ってみた

2026.06.18

製造ビジネステクノロジー部の小林です。

今回は、npm workspaces を使ったモノレポについて紹介します。

モノレポって何?

モノレポ(Monorepo) とは、複数のプロジェクト(パッケージ)を 一つのリポジトリ で管理する開発スタイルのことです。

対義語は ポリレポ(Polyrepo) で、こちらはプロジェクトごとにリポジトリを分ける従来のスタイルです。

図で比較

ポリレポ(リポジトリがバラバラ)

my-frontend/ フロントエンド用リポジトリ
my-backend/ バックエンド用リポジトリ
my-infrastructure/ インフラ用リポジトリ
my-tests/ テスト用リポジトリ

モノレポ(1 つにまとまっている)

my-app.git/ リポジトリは「1つ」だけ
├── packages/
   ├── frontend/ フロントエンド
   ├── backend/ バックエンド
   ├── infrastructure/ インフラ
   └── tests/ テスト
└── package.json 全体の設定

モノレポで嬉しいこと

1. 共通の型定義を変えても、すぐ全体に反映

ポリレポでは、共通の型定義を変更した場合、各リポジトリで依存を更新する手順が必要になります。モノレポなら同じリポジトリ内なので変更が即座に全体へ反映されます。

2. バージョンの整合性を保ちやすい

ポリレポでは「バックエンドは React 18、フロントエンドは React 19」のように、知らないうちにバージョンがズレてしまうこともあります。
モノレポなら、依存関係を一元管理できるので、バージョンを揃えやすいです。

3. 関連する変更を 1 つのプルリクエストにまとめられる

ポリレポでは「ログイン機能の追加」のような 1 つの変更でも、フロント・バック・テストでプルリクエストが分散し、レビューが追いづらくなりがちでした。

モノレポなら、関連する変更を 1 つのプルリクエストにまとめられるので、レビューがスムーズになり、変更の全体像も把握しやすくなります。

4. CI/CD 設定をまとめてメンテナンスできる

ポリレポでは、似たような CI/CD 設定を各リポジトリに書き、修正のたびに全部直す必要がありました。
モノレポなら、CI/CD 設定を共通化できるので、メンテナンスは 1 箇所で OK です。

5. 依存関係の見通しがよい

ポリレポでは package-lock.json がリポジトリの数だけ存在し、依存の差分を追うのが大変でした。
モノレポなら、ロックファイルを 1 つに集約できるので、依存関係の全体像がひと目でつかめます。

npm workspaces とは

npm workspaces は、npm v7 から標準搭載されたモノレポ管理機能です。追加のツールを入れなくても、npm だけでモノレポを始められるのが大きな魅力です。

https://docs.npmjs.com/cli/v11/using-npm/workspaces

npm workspaces の特徴

1. 追加ツール不要

Lerna や Yarn などを別途インストールしなくても、npm だけで完結します。モノレポを「とりあえず始めてみたい」ときに、ハードルが低いのはメリットです。

2. 依存関係の巻き上げ

共通の依存パッケージは、ルートの node_modules にまとめて集約されます。同じパッケージを何度もインストールせずに済むので、ディスク容量を節約できます。

3. パッケージ間の参照が簡単

パッケージ同士がシンボリックリンクで自動的につながるため、ローカルのパッケージをすぐに参照できます。「別パッケージの関数を使いたい」が、まるで普通の import のように書けます。

4. 一括コマンド実行

--workspaces フラグを使えば、全パッケージに対してまとめてコマンドを実行できます。
1 つずつフォルダを移動して実行する必要がなく、ルートから一気に処理できるのが便利です。

# 全パッケージの test スクリプトを一括実行
npm test --workspaces

# 短縮形(-ws)でもOK
npm test -ws

ちなみに特定のパッケージだけ実行する場合は下記のようにします。

# frontend パッケージだけテストを実行
npm test --workspace=frontend

# 短縮形(-w)でもOK
npm test -w frontend

依存パッケージのインストールにも使えます。 install でもワークスペースを指定できます。

# frontend パッケージに axios を追加
npm install axios --workspace=frontend

# ルート全体に共通の開発ツールを追加(-w を付けない)
npm install -D typescript

5. 単一の package-lock.json

ロックファイルがリポジトリ全体で 1 つに統一されます。

スクリーンショット 2026-06-18 0.12.05

6. 他の選択肢との違い

npm workspaces 以外にも、モノレポ管理の選択肢はいくつかあります。

ツール 特徴
npm workspaces npm 標準。学習コストが低く始められる
Yarn workspaces Yarn ベースのワークスペース管理
pnpm workspaces 後述の 「phantom dependency」問題を根本解決
Nx / Turborepo ビルドキャッシュなど高度な機能を提供

npm workspaces の使い方

実際にモノレポをセットアップする流れをみていきます。

セットアップの全体像

my-app/ ← ① ルートで workspaces を宣言
├── package.json
└── packages/
├── frontend/ ← ② 各パッケージを作成
│ └── package.json
├── backend/
│ └── package.json
└── infrastructure/
└── package.json

この構成を作って、最後に npm install を 1 回叩くだけで準備完了です。

1. ルートの package.json で宣言する

まず、ルートの package.json に「どこにパッケージがあるか」を書きます。

{
  "name": "my-app",
  "private": true,
  "workspaces": [
    "packages/frontend",
    "packages/backend",
    "packages/infrastructure"
  ]
}

ポイントは 2 つです。

設定 意味
"private": true ルート自体は公開しないための設定(workspaces 利用時はほぼ必須)
"workspaces" サブパッケージのパスを並べるだけ

ワイルドカードも使えます。パッケージが増えても書き直す必要がありません。

"workspaces": ["packages/*"]

2. 各パッケージを作成する

次に、それぞれのパッケージのフォルダを作って初期化します。

mkdir -p packages/frontend
cd packages/frontend
npm init -y

各パッケージは、それぞれ独立した package.json を持ちます。パッケージごとに名前・依存・スクリプトを管理できます。

3. ルートで npm install する

ルートに戻って、npm install を 1 回実行します。

npm install

これだけで、全パッケージの依存が一気に解決されます。 裏側では次の 2 つが起きています。

  1. 外部依存(npm パッケージ)→ ルートに集約
my-app/
└── node_modules/ ここに物理的にまとめて配置
    ├── react/
    ├── axios/
    └── ...

共通パッケージがルートに巻き上げられ、ディスクを節約できます。

  1. ワークスペース同士 → シンボリックリンクでつながる
my-app/
└── node_modules/
    └── frontend  →(リンク)→  packages/frontend

ルートの node_modules から各パッケージへショートカット(シンボリックリンク)が張られます。これが import でローカルパッケージを参照できる仕組みです。

4. パッケージを指定してコマンド実行

セットアップ後は、ルートから各パッケージを自在に操作できます。

# 特定のワークスペースだけ実行
npm run build --workspace=frontend

# 全ワークスペースで実行(スクリプトがあるものだけ)
npm run check:type --workspaces --if-present

# 依存追加もワークスペース指定でOK
npm install axios --workspace=backend

⚠️ --workspace の値に注意
指定する値は、package.json の name フィールド(または相対パス)です。
スコープ付き(@my-app/frontend)の場合は、スコープも含めて指定します。

npm run build --workspace=@my-app/frontend

何が嬉しいの?

共通ツールをルートに集約できる

ESLint・Prettier・TypeScript・cspell など、「全パッケージで同じバージョン・同じ設定を使いたい」ツールをルートにまとめて置けます。

// ルートの package.json(抜粋)
{
  "devDependencies": {
    "typescript": "6.0.3",
    "eslint": "10.4.0",
    "prettier": "3.8.3",
    "cspell": "10.0.0"
  }
}

各パッケージはこれを参照するだけ。「フロントとバックで ESLint のバージョンがズレてた…」みたいな問題が起きにくくなります。

横断的なチェックがワンコマンドで完結

リポジトリ全体のチェックを、ルートから一発で実行できます。

npm run check:type    # 全パッケージの tsc --noEmit を一気に実行
npm run check:lint    # リポジトリ全体に eslint
npm run check:format  # リポジトリ全体に prettier

CI でも開発時でも、ルートで一発叩けば全部チェックされるのは便利です。

まとめて変更ができる

「API の型を変えたから、それを使うフロントもバックも一緒に直す」という変更を 1 つのプルリクエストでまとめられます。ポリレポだとリポジトリ間の整合性を取るのが大変ですが、モノレポなら CI も同じワークフローで回せます。

やってみた

実際に、AWS 上で動く Web アプリを npm workspaces で構築してみました。バックエンド・フロントエンド・インフラ・テストの 4 つのパッケージで構成しています。

ディレクトリ構成

my-monorepo-app/
├── packages/
   ├── backend/          # API サーバー(Lambda)
   ├── frontend/         # React
   ├── infrastructure/   # AWS CDK
   └── backend-tests/    # Playwright
├── eslint.config.js      # 共通 ESLint 設定
├── .prettierrc.json      # 共通 Prettier 設定
├── cspell.json           # 共通スペルチェック設定
├── tsconfig.json         # 共通 TypeScript 設定
└── package.json          # workspaces 定義

設定ファイルをルートに集約することで、全パッケージで同じルールを共有できます。

ルートの package.json

ルートに workspaces の定義と、リポジトリ全体を横断するチェック用スクリプトをまとめます。

{
  "name": "my-monorepo-app",
  "private": true,
  "workspaces": [
    "packages/infrastructure",
    "packages/backend",
    "packages/backend-tests",
    "packages/frontend"
  ],
  "scripts": {
    "check:format": "prettier --check --cache \"./**/*.{ts,tsx}\"",
    "check:lint": "eslint . --cache",
    "check:type": "npm run check:type --workspaces --if-present",
    "check:spell": "cspell \"**/*\" --cache --gitignore"
  }
}

スクリプトのポイント

設定 意味
--workspaces 全パッケージに対して実行
--if-present そのスクリプトが無いパッケージはスキップ

各パッケージ側のスクリプト

各パッケージ側には、ルートから呼び出される check:type を定義しておきます。

// packages/backend/package.json(抜粋)
{
  "name": "backend",
  "scripts": {
    "check:type": "tsc --noEmit"
  }
}

これで、ルートで npm run check:type を叩くと、全パッケージの型チェックが一気に走ります。

npm run check:type
# → backend, frontend, infrastructure... の tsc --noEmit がまとめて実行される

各パッケージの役割

パッケージ 役割 主要ライブラリ
backend API サーバー Express, Prisma, AWS SDK, Zod
frontend SPA React, Vite, Jotai
infrastructure IaC AWS CDK
backend-tests E2E / 統合テスト Playwright

ポイント 1:チェックは横断、テストは個別

# 全パッケージ横断のチェック
npm run check:format
npm run check:lint
npm run check:type
npm run check:spell

# テストは各パッケージで(時間かかるので必要なものだけにする)
cd packages/backend && npm run test:ut
cd packages/frontend && npm run test:ut

ユニットテストは時間がかかるので、変更したパッケージだけ叩く運用にしています。

ポイント 2:TypeScript はルートにも、各パッケージにも明示する

各パッケージの devDependencies にも typescript: 6.0.3 を入れていますが、ルートにも同じバージョンを置いています。理由は 3 つあります。

  1. ルートから直接 tsc を叩くケースがある:ルートで tsc --noEmit --workspaces のように一括で型チェックする
  2. ルートツールが TS バージョンを参照する:typescript-eslint は ESLint 実行時に TS を解決する
  3. phantom dependency を避ける:使うなら必ず自身の package.json に明示する(詳細は後述)

3 点目が特に大事で、「巻き上げで動くから」と省略すると、ハマりどころに直結します。

ポイント 3:--workspaces --if-present が便利

"check:type": "npm run check:type --workspaces --if-present"

このスクリプトは、全パッケージの check:type をまとめて実行します。ここで効いてくるのが --if-present フラグです。
--if-present を付けると、check:type スクリプトを持たないパッケージは自動でスキップされます。

npm run check:type --workspaces --if-present

  ├── backend check:type あり 実行
  ├── frontend check:type あり 実行
  ├── infrastructure check:type なし  ⏭️ スキップ
  └── backend-tests check:type あり 実行

https://docs.npmjs.com/cli/v11/using-npm/workspaces#ignoring-missing-scripts

スクリプトが無いパッケージに当たった時点でエラーで止まってしまいます。

ポイント 4:tsconfig.jsonextends で共通化

TypeScript の設定も、ルートに共通のベース設定を置いて、各パッケージから extends で継承できます。

https://www.typescriptlang.org/tsconfig/#extends

イメージ

my-monorepo-app/
├── tsconfig.json 共通のベース設定(みんなが継承)
└── packages/
    ├── backend/
   └── tsconfig.json extends でベースを継承 個別設定
    └── frontend/
        └── tsconfig.json extends でベースを継承 個別設定

ルートに共通設定を置く

全パッケージで共通して使いたい設定を、ルートの tsconfig.json にまとめます。

// ルートの tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

各パッケージで extends して継承する

各パッケージは、extends でルートの設定を読み込み、そのパッケージ固有の設定だけを追記します。

// packages/backend/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

この backend の設定は、実際には次のように展開されます。

{
  // ルートから継承された設定
  "target": "ES2022",
  "module": "NodeNext",
  "strict": true,
  "esModuleInterop": true,
  "skipLibCheck": true,
  // backend で追記した設定
  "outDir": "./dist",
  "rootDir": "./src"
}

ハマりどころ

実運用してみて感じた注意点も共有します。

ローカル開発では npm install を推奨

npm cinode_modules を一度消してから lockfile 通りにクリーンインストールするコマンドなので、差分更新がしたい開発中には向きません。(package-lock.json の整合性チェックも兼ねるので CI では便利)。

巻き上げの罠(phantom dependency)

npm workspaces の「依存の巻き上げ(ホイスティング)」には、便利な反面、気づきにくい落とし穴があります。それが phantom dependency(幽霊依存) です。

巻き上げによって、共通の依存はルートの node_modules に集約されます。
このとき、自分の package.json に書いていないパッケージまで import できてしまうのです。

例えば backendpackage.json には書いていないのに、frontend が依存している axios がルートに巻き上げられているおかげで backend でも import axios from 'axios' が動いてしまう、というケースです。

これをしてしまうと、frontend から axios を削除した瞬間、巻き上げが消えて backend が突然動かなくなるということが起こります。

意図しない依存が混入しないよう、各パッケージで使うものは必ずそのパッケージの package.json に書きましょう。

参考リンク

この記事をシェアする

関連記事