1. はじめに
本記事を作成するきっかけとなったのが、
モノレポ自体はスクールの同期が業務委託の案件をモノレポ構成で進めていたのを知り、
同一レポジトリでフロントエンドとバックエンドのソース管理が出来るというのが
魅力的に感じました。
4月に卒業したプログラミングスクールでPlayGroundという
自分の作りたいアプリを設計・開発・運用まで1人でやりきり、
それを月に1度アウトプットとして記事を上げて、
LP会で進捗報告を行うという形式のラボに参加する際、
アプリのシステム構成として、モノレポ構成で進めることに決めたのがきっかけです。
本記事は学習記録として、当方が作成したプロジェクトをベースに
記載した記事のため、閲覧した方の意図にそぐわない内容もあります。
フロントエンド:Next.js バックエンド:Go(Echo)
2.モノレポとは
モノレポ(Monorepo)とは、複数の独立したプロジェクトのソースコードを、
単一のGitリポジトリで管理するソフトウェア開発戦略。
例えば、当方のプロジェクトではWebフロントエンド(apps/web)、
Go言語のAPIサーバー(apps/api)、そしてそれらで共有される
UIコンポーネント(packages/ui)や
設定ファイル(packages/tailwind-configなど)が、
全て一つのリポジトリに収められています。
主な特徴
- 単一のリポジトリ:全てのコードが1つのGitリポジトリに格納されます。
- コードの共有:プロジェクト間でコードを容易に共有が可能
- 一貫性のある開発:リンターやフォーマッターなどの設定を共通化することで、
一貫性のある開発体験を構築できます。 - 変更の追跡が容易:全プロジェクトの変更履歴を単一のリポジトリで確認できます。
メリット・デメリット
メリット
✅ 依存関係の一元管理
プロジェクト全体の依存関係がルートのpnpm-lock.yaml(またはyarn.lock, package-lock.json)ファイル一つで管理されます。
これにより、パッケージ間のバージョンの不整合や
競合(いわゆる「依存関係地獄」)を防ぎやすくなります。
✅ 大規模なリファクタリングが容易
APIの変更がフロントエンドに影響を及ぼすような場合でも、
関連する全ての変更を単一のコミット(アトミックなコミット)として
完結させることができます。
複数のリポジトリにまたがる変更を調整する必要がありません。
✅ 統一された開発体験
ビルド、テスト、リンティングなどの開発コマンドや
CI/CDパイプラインをプロジェクト全体で標準化できます。
新しい開発者が参加した際も、単一のリポジトリをクローンし、
共通のコマンドを実行するだけで開発環境をセットアップできます。
デメリット
🚫 ビルド・テスト時間の増大
プロジェクトが大規模になるにつれて、すべてのコードをビルドしたり、
すべてのテストを実行したりするのに時間がかかるようになります。
少しの変更でも、関連しない部分までCIが実行されてしまう可能性があります。
(この課題を解決するのがこの後説明するTurborepoです)
🚫 リポジトリサイズの肥大化
すべてのコードとその履歴が単一のリポジトリに集約されるため、
リポジトリのサイズが大きくなり、初回のgit cloneに時間がかかることがあります。
🚫 複雑な権限管理
パッケージやアプリケーションごとにアクセス権限を細かく設定したい場合、モノレポ標準の機能だけでは実現が難しく、追加の仕組みが必要になることがあります。
■モノレポとマルチレポの違い
3.Turborepoとは
Turborepoは、Vercel(Next.jsの開発元)によって開発された、
JavaScript/TypeScriptモノレポのための高性能なビルドシステムです。
モノレポのデメリットである「ビルド・テスト時間の増大」を、
強力なキャッシュ機構と効率的なタスク実行によって解決します。
メリット・デメリット
メリット
✅ 劇的に高速なビルド(キャッシュ機能)
Turborepoの最大の特徴は、一度実行したタスクの成果物をキャッシュする機能です。
-
ローカルキャッシュ: 自分のPC内で、
変更のないコードに対するタスク(ビルド、テストなど)を
スキップし、前回の結果を即座に再利用します。 -
リモートキャッシュ: Vercelなどのクラウドサービスと連携し、
キャッシュをチーム全体で共有します。CI/CDや、
他のチームメンバーが実行したタスクの結果を自分のPCで再利用でき、
ビルド時間を劇的に短縮します。
✅ 効率的なタスク実行(パイプライン)
turbo.jsonファイルで、
各タスク(dev, build, testなど)間の依存関係を定義できます。
例えば、
「webアプリのビルドは、uiライブラリのビルドが終わってから実行する」といった
依存関係を記述することで、Turborepoが最適な順序でタスクを並列実行してくれます。
デメリット
🚫 学習コスト
turbo.jsonのパイプライン設定や--filterのようなコマンドオプション、
パイプライン設計の習得など、Turborepo特有の概念を学ぶ必要があります。
しかし、基本的な使い方であればドキュメントも充実しており、
比較的容易に導入できます。
🚫 JavaScript/TypeScript中心
元々JavaScript/TypeScriptエコシステムのために作られたツールですが、
package.jsonのscriptsにコマンドを定義すれば、
Go(本プロジェクトのように)やRustなど、
他の言語で書かれたプロジェクトもタスクランナーとして管理することが可能です
🚫 キャッシュ管理の複雑さ
-
キャッシュヒットしない場合の調査:
Turborepoの最も困難な側面の一つは、
期待通りにキャッシュがヒットしない場合のトラブルシューティングです。
キャッシュミスが発生すると、
ビルド時間の短縮というTurborepoの主要な利点が失われてしまいます。 -
環境変数とキャッシュの関係理解
環境変数の扱いは、Turborepoのキャッシュ戦略において特に複雑な領域です。
環境変数の値が変わるとキャッシュキーが変更され、
予期しないキャッシュミスが発生することがあります。
3.Turborepoの環境構築
3.1 pnpmインストール(事前準備)
Turborepoがpnpmを推奨しているので、pnpmをインストール
brew install pnpm
※インストール方法は複数パターンあります。(公式ドキュメント参照)
3.2 Turborepoのプロジェクト作成
pnpm dlx create-turbo@latest <my-turborepo>
Where would you like to create your Turborepo? (./my-turborepo)
↑ プロジェクト名を入力する。
この後にパッケージマネージャーの選択(npm, pnpm, yarn, Bun)の選択を行う。
上記コマンドを実行すると下記のディレクトリ構成でプロジェクト作成されます。
my-turborepo/
├── apps/
│ ├── web/ # Next.jsアプリケーション
│ └── docs/ # ドキュメントサイト
├── packages/
│ ├── ui/ # 共有UIコンポーネント
│ ├── eslint-config/ # ESLint設定
│ └── typescript-config/ # TypeScript設定
├── .gitignore # Github連携除外設定
├── .npmrc # npm設定(自分は使ってないです)
├── turbo.json # Turboタスク設定
├── package.json # ルートパッケージ設定
└── pnpm-workspace.yaml # pnpmワークスペース設定
3.3 初期設定
■プロジェクトルートのpackage.json
{
"name": "project-name",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types"
},
"devDependencies": {
"prettier": "^3.6.2",
"turbo": "^2.5.5",
"typescript": "5.8.3"
},
"packageManager": "pnpm@9.0.0",
"engines": {
"node": ">=18"
}
}
■pnpm-workspace.yaml(初期設定値)
packages:
- "apps/*"
- "packages/*"
■ディレクトリ構造の作成
# バックエンド用ディレクトリを作成
mkdir -p apps/api
3.2 Next.jsプロジェクト作成
pnpm dlx create-turbo@latestを実行すると
自動でNext.jsプロジェクトが作成されているので、
改めてのプロジェクト作成は不要。
3.3 Goプロジェクト作成
■Goのインストール
下記公式ドキュメントよりGoパッケージをダウンロード→インストール
■Goプロジェクト初期化
# バックエンド用ディレクトリに移動
cd apps/api
# Goプロジェクト作成
go mod init
# Echoインストール
go install github.com/labstack/echo/v4
# Airインストール
go install github.com/cosmtrek/air@latest
■apps/api/package.jsonの作成
{
"name": "api",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "go run main.go",
"build": "mkdir -p ./tmp && go build -o ./tmp/main .",
"test": "go test ./...",
"test:coverage": "mkdir -p coverage && go test -covermode=atomic -coverprofile=coverage/coverage.out ./...",
"lint": "golangci-lint run",
"clean": "rm -rf ./tmp ./coverage"
}
}
■main.goの作成
package main
import (
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// ミドルウェアの設定
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// ルートエンドポイント
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
// ヘルスチェック
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
})
// ポート設定
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
e.Logger.Fatal(e.Start(":" + port))
}
■Air設定ファイル(.air.toml)の作成
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_root = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
■turbo.json の設定
{
"$schema": "https://turborepo.com/schema.json",
"ui": "tui",
"globalEnv": ["BACKEND_INTERNAL_URL"],
"globalPassThroughEnv": ["NODE_ENV", "CI"],
"globalDependencies": ["package.json", "pnpm-lock.yaml"],
"tasks": {
"build": {
"dependsOn": ["^build", "check-types"],
"inputs": [
"$TURBO_DEFAULT$",
".env*",
"!**/*.test.*",
"!**/*.spec.*",
"!node_modules/**"
],
"outputs": [".next/**", "!.next/cache/**", "dist/**", "tmp/**"],
"env": ["BACKEND_INTERNAL_URL"],
"passThroughEnv": ["NODE_ENV"]
},
"dev": {
"cache": false,
"persistent": true,
"env": ["BACKEND_INTERNAL_URL"],
"passThroughEnv": ["NODE_ENV", "PORT"]
},
"lint": {
"dependsOn": ["^lint"],
"inputs": ["src/**/*.{ts,tsx,js,jsx}", ".eslintrc.*", "eslint.config.*"],
"outputs": []
},
"check-types": {
"dependsOn": ["^check-types"],
"inputs": ["src/**/*.{ts,tsx}", "tsconfig.json", "**/*.d.ts"],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"inputs": [
"src/**/*.{ts,tsx,js,jsx}",
"**/*.test.*",
"**/*.spec.*",
"jest.config.*"
],
"outputs": ["coverage/**"],
"env": ["NODE_ENV"],
"passThroughEnv": ["NODE_ENV"]
},
"clean": {
"cache": false
}
}
}
■依存関係の整理
# 全ワークスペースの依存関係をインストール
pnpm install
# Goモジュールの依存関係をインストール
cd apps/api
go mod tidy
■開発サーバー起動
# Next.jsとGoを並列起動
pnpm dev
# Next.jsとGoを並列ビルド
pnpm build
pnpm --filter web dev # Next.jsのみ起動
pnpm --filter api dev # Go APIのみ起動
pnpm --filter web test:e2e # E2Eテストのみ実行
3.4 最終的なプロジェクトディレクトリ構成
Docker構築もしてますが、今回はTurborepoの環境構築メインのため、
手順は割愛します。
.
├── apps # 実際にデプロイされるアプリケーションを格納するディレクトリ
│ ├── api # Go言語で実装されたバックエンドAPIサーバー
│ │ ├── Dockerfile # 本番環境用のDockerfile
│ │ ├── main.go # アプリケーションのエントリーポイント
│ │ ├── go.mod # Goの依存関係を管理するファイル
│ │ └── package.json # TurborepoのタスクからGoのコマンドを実行するために配置
│ └── web # Next.jsで実装されたフロントエンドWebアプリケーション
│ ├── app/ # Next.js App Routerのメインディレクトリ
│ │ └── layout.tsx # アプリケーション全体の共通レイアウト
│ ├── package.json # フロントエンドの依存関係とスクリプトを定義
│ └── next.config.mjs # Next.jsの各種設定を行うファイル
├── packages # 複数のアプリケーション間で共有されるコードや設定を格納するディレクトリ
│ ├── eslint-config # プロジェクト共通のESLint(コード静的解析ツール)設定
│ │ ├── base.js # ESLintの基本ルールを定義
│ │ └── package.json
│ ├── tailwind-config # プロジェクト共通のTailwind CSS(CSSフレームワーク)設定
│ │ ├── postcss.config.js # PostCSS(CSS変換ツール)の設定
│ │ └── package.json
│ ├── typescript-config # プロジェクト共通のTypeScript設定
│ │ ├── base.json # tsconfig.jsonの基本設定
│ │ └── package.json
│ └── ui # アプリケーション間で共有されるReactコンポーネントライブラリ
│ ├── src/ # コンポーネントのソースコード
│ │ └── button.tsx # 例:共通のボタンコンポーネント
│ └── package.json
├── docker-compose.yml # 開発環境をDockerコンテナで一括起動するための設定ファイル
├── package.json # プロジェクト全体の開発用依存関係と、Turborepoを実行するスクリプトを定義
├── pnpm-workspace.yaml # pnpmに対して、モノレポ内のパッケージの場所を教える設定ファイル
└── turbo.json # Turborepoのパイプライン(タスク間の依存関係やキャッシュ)を設定するファイル
4 まとめ
AIとやり取りしながらですが、
一からの環境構築手順を整理出来たので、
別のアプリでGo+Next.jsで作成する際に、
本記事をベースに一から環境構築を行い、理解を深めたいと思います。