はじめに
モノレポ(モノリポジトリ)は、複数のプロジェクトを1つのリポジトリで管理する手法です。
この記事では、Express(バックエンド)と React(フロントエンド)を使ったWebアプリケーションを TypeScript で開発するためのモノレポ環境を、npm のワークスペース機能を使って構築します。
開発環境
開発環境は以下の通りです。
- Windows11
- VSCode
- Node.js 22.18.0
- npm 11.5.2
- TypeScript 5.9.2
- Express 5.1.0
- React 19.1.1
- Vite 7.1.3
モノレポ構成
今回構築するプロジェクト構成は以下のようになります。
monorepo-npm/
├── package.json # ルートの package.json(ワークスペース設定)
├── tsconfig.json # 共通の TypeScript 設定
├── packages/
│ ├── backend/ # Express アプリケーション
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ └── frontend/ # React アプリケーション
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
└── shared/ # 共通の型定義など
├── package.json
├── tsconfig.json
└── src/
プロジェクトフォルダの作成
まずモノレポ用に monorepo-npm というフォルダを作成し、その中に必要なフォルダ構成を作成します。
mkdir monorepo-npm
cd monorepo-npm
mkdir packages shared
mkdir packages/backend packages/frontend
mkdir packages/backend/src packages/frontend/src shared/src
ルートの設定
package.json 作成
モノレポのルートディレクトリで、package.json を作成します。
npm init -y
作成した package.json を開発・ビルド用の設定のみを含む内容に編集します。
{
"name": "monorepo-npm",
"version": "1.0.0",
"type": "module",
"private": true,
"workspaces": [
"packages/*",
"shared"
],
"scripts": {
"build": "npm run build --workspaces --if-present",
"dev": "concurrently \"npm run dev -w @monorepo-npm/shared\" \"npm run dev -w @monorepo-npm/backend\" \"npm run dev -w @monorepo-npm/frontend\"",
"clean": "npm run clean --workspaces --if-present",
"build:shared": "npm run build -w @monorepo-npm/shared",
"build:backend": "npm run build -w @monorepo-npm/backend",
"build:frontend": "npm run build -w @monorepo-npm/frontend",
"test": "npm run test --workspaces --if-present"
},
"devDependencies": {
"concurrently": "^9.2.1",
"typescript": "^5.9.2"
},
"engines": {
"node": ">=22.18.0",
"npm": ">=11.5.2"
}
}
"workspaces" は一つのリポジトリ内で複数のパッケージを効率的に管理するための仕組みです。
ルートの package.json に "workspaces" 配列を定義することで、どのディレクトリが独立したパッケージ(ワークスペース)であるかを npm に教えます。今回設定したディレクトリの役割は以下の通りです。
-
"packages/*": packages フォルダ内のすべてのサブディレクトリを自動認識 -
"shared": 共通ライブラリを独立したワークスペースとして管理
各ワークスペースは、他のワークスペースを npm レジストリにある外部パッケージのように依存関係として宣言できます。また、 ルートディレクトリで npm run コマンドを実行する際に、特定のワークスペースを指定したり、すべてのワークスペースに対してまとめて実行したりできます。
scripts の dev コマンドでは、以下のように複数サーバーを同時起動するようにしています。
- shared: TypeScriptの監視ビルド
- backend: Express開発サーバー
- frontend: Vite開発サーバー
ルートの package.json で TypeScript をインストールすれば、モノレポ内のすべてのワークスペースで TypeScript バージョンを統一し、管理を簡素化できます。
各ワークスペースで TypeScript をインストールする必要はありません。、各ワークスペースの tsconfig.json で、ルートにある共通の tsconfig.json を "extends" を使って参照します。
tsconfig.json 作成
プロジェクト全体の共通設定として、ルートに tsconfig.json を作成します。
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"exclude": ["node_modules", "dist"]
}
共通パッケージ(shared)の作成
共通パッケージは、フロントエンドとバックエンドで共有する型定義やユーティリティを管理します。
package.json 作成
cd shared
npm init -y
shared/package.json を以下のように編集します。
{
"name": "@monorepo-npm/shared",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"dev": "tsc --watch"
},
"engines": {
"node": ">=22.18.0"
}
}
name フィールドはスコープ(@monorepo-npm/)付きパッケージ名にすることで、以下の効果があります。
- パッケージ名の衝突を回避
- モノレポ内のパッケージを明確に識別
- import 文での可読性向上
exports フィールドでは、以下を明示的に指定しています。
-
types: TypeScript型定義ファイルの場所 -
import: ESModules 用のエントリポイント
この設定により、他のパッケージから import { Type } from '@monorepo-npm/shared' で import できます。
tsconfig.json 作成
shared/tsconfig.json を作成します。
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]
}
TypeScript のプロジェクト参照 (project references) という機能で他のパッケージから参照できるように "composite": true を設定しています。
共通の型定義作成
shared/src/types.ts を作成し、フロントエンドとバックエンドで共通して使用する型を定義します:
export interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
export interface PaginationParams {
page: number;
limit: number;
}
shared/src/index.ts を作成し、エクスポートします:
export * from './types.js';
バックエンド開発環境構築
package.json 作成
cd ../packages/backend
npm init -y
packages/backend/package.json を以下のように編集します:
{
"name": "@monorepo-npm/backend",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"clean": "rm -rf dist"
},
"dependencies": {
"@monorepo-npm/shared": "*",
"express": "^5.1.0",
"cors": "^2.8.5"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/cors": "^2.8.0",
"@types/node": "^22.0.0",
"tsx": "^4.0.0"
},
"engines": {
"node": ">=22.0.0"
}
}
"dependencies" の "@monorepo-npm/shared" で "*" を使用することで、常にワークスペース内の最新版を参照します。これにより、以下のような効果があります。
- バージョン管理の複雑さを回避
- ローカル開発での即座の変更反映
- 循環依存の問題を回避
tsconfig.json 作成
packages/backend/tsconfig.json を作成します。
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"],
"references": [
{
"path": "../../shared"
}
]
}
shared の設定でも出てきた TypeScript のプロジェクト参照 (project references) という機能を利用して、"references" でパッケージ間の依存関係を明示しています。この設定により、以下のような効果があります。
- 増分ビルド: shared パッケージが変更された時のみ再ビルド
- 型チェックの最適化: 依存関係を理解した効率的な型チェック
- エディタサポート: Go to Definition 機能で shared パッケージ内へジャンプ可能
Express サーバー作成
packages/backend/src/index.ts を作成します。
import express, { Express, Request, Response } from 'express';
import cors from 'cors';
import { User, ApiResponse } from '@monorepo-npm/shared';
const app: Express = express();
const port = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
// サンプルデータ
const sampleUsers: User[] = [
{
id: '1',
name: '田中太郎',
email: 'tanaka@example.com',
createdAt: new Date('2023-01-01')
},
{
id: '2',
name: '佐藤花子',
email: 'sato@example.com',
createdAt: new Date('2023-01-02')
}
];
app.get('/', (req: Request, res: Response) => {
res.json({ message: 'Hello from backend!' });
});
app.get('/api/users', (req: Request, res: Response<ApiResponse<User[]>>) => {
res.json({
success: true,
data: sampleUsers
});
});
app.get('/api/users/:id', (req: Request, res: Response<ApiResponse<User>>) => {
const { id } = req.params;
const user = sampleUsers.find(u => u.id === id);
if (!user) {
res.status(404).json({
success: false,
error: 'User not found'
});
return;
}
res.json({
success: true,
data: user
});
});
app.listen(port, () => {
console.log(`Backend server running on port ${port}`);
});
フロントエンド開発環境構築
package.json 作成
cd ../frontend
npm init -y
packages/frontend/package.json を以下のように編集します:
{
"name": "@monorepo-npm/frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite",
"preview": "vite preview",
"clean": "rm -rf dist"
},
"dependencies": {
"@monorepo-npm/shared": "*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"vite": "^6.0.0"
},
"engines": {
"node": ">=22.0.0"
}
}
tsconfig.json 作成
packages/frontend/tsconfig.json を作成します:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"outDir": "./dist",
"rootDir": "./src",
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"],
"references": [
{
"path": "../../shared"
}
]
}
Vite 設定
packages/frontend/vite.config.ts を作成します:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true
}
}
},
build: {
outDir: 'dist'
}
});
HTML テンプレート
packages/frontend/index.html を作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Monorepo Npm App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
React アプリケーション作成
packages/frontend/src/main.tsx を作成します。
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (!container) {
throw new Error('Root element not found');
}
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
packages/frontend/src/App.tsx を作成します。
import React, { useEffect, useState } from 'react';
import { User, ApiResponse } from '@monorepo-npm/shared';
const App: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch('/api/users');
const result: ApiResponse<User[]> = await response.json();
if (result.success && result.data) {
setUsers(result.data);
} else {
setError(result.error || 'データの取得に失敗しました');
}
} catch (err) {
setError('ネットワークエラーが発生しました');
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return (
<div style={{ padding: '20px' }}>
<h1>ユーザー一覧</h1>
<ul>
{users.map(user => (
<li key={user.id} style={{ marginBottom: '10px' }}>
<strong>{user.name}</strong> - {user.email}
<br />
<small>登録日: {new Date(user.createdAt).toLocaleDateString()}</small>
</li>
))}
</ul>
</div>
);
};
export default App;
依存関係のインストール
ルートディレクトリに戻って、すべての依存関係をインストールします:
cd ../..
npm install
このコマンドにより、ワークスペースの設定に基づいて各パッケージの依存関係が自動的にインストールされます。
ビルドとスクリプト設定
ルートのpackage.json にスクリプト追加
開発を効率化するために、ルートの package.json にスクリプトを追加します:
{
"scripts": {
"build": "npm run build --workspaces --if-present",
"dev": "concurrently \"npm run dev -w @monorepo-npm/shared\" \"npm run dev -w monorepo-npm/backend\" \"npm run dev -w @monorepo-npm/frontend\"",
"clean": "npm run clean --workspaces --if-present",
"build:shared": "npm run build -w @monorepo-npm/shared",
"build:backend": "npm run build -w @monorepo-npm/backend",
"build:frontend": "npm run build -w @monorepo-npm/frontend",
"test": "npm run test --workspaces --if-present"
}
}
npm v11では --if-present フラグを使用することで、スクリプトが存在しないワークスペースでエラーが発生することを防げます。
動作確認
共通パッケージのビルド
まず共通パッケージをビルドします。
npm run build:shared
開発サーバーの起動
すべてのパッケージの開発サーバーを同時に起動します。
npm run dev
このコマンドにより以下が同時に実行されます。
- 共通パッケージの監視ビルド
- バックエンドサーバー(ポート3001)
- フロントエンド開発サーバー(ポート3000)
ブラウザで http://localhost:3000 にアクセスすると、バックエンドからユーザーデータを取得して表示するReactアプリケーションが確認できます。
モノレポの利点
この構成により以下の利点が得られます。
- 型の共有: 共通パッケージにより、フロントエンドとバックエンド間で型定義を共有
- 一括管理: 単一のリポジトリですべてのコードを管理
- 効率的な開発: 関連するパッケージを同時に開発・テスト可能
- 依存関係の統一: プロジェクト全体で依存関係のバージョンを統一
おわりに
この記事では、 npm のワークスペース機能を使って Express / React / TypeScriptによるモノレポ環境を構築しました。共通の型定義を使うことで、フロントエンドとバックエンド間でタイプセーフな開発が可能になります。
