この記事で得られること
- Next.js + Go のモノレポ構成パターン
- pnpm workspace + Turborepo の実践的な使い方
- 4ポータルで共通UIを使い回す設計
- ソロ開発で破綻しないパッケージ分割の考え方
はじめに
第1回で紹介した通り、Saruは4階層のアカウント構造を持つマルチテナントSaaSだ。これを実現するために、4つのフロントエンド + 4つのバックエンドAPIという構成を採用している。
普通に考えると、8つのリポジトリを管理することになる。ソロ開発では破綻する。
そこでモノレポを採用した。本記事では、その構成と設計判断を解説する。
1. なぜNext.js × Go なのか
技術選定の理由
| 領域 | 技術 | 選定理由 |
|---|---|---|
| Frontend | Next.js 14 | App Router、RSC、豊富なエコシステム |
| Backend | Go + Echo | シンプル、高速、型安全、デプロイが楽 |
| DB | PostgreSQL | RLSによるマルチテナント分離 |
なぜフルスタックフレームワーク(Next.js API Routes)を使わないのか?
- 関心の分離: フロントエンドとバックエンドのデプロイサイクルを分けたい
- 言語の強み: Goの方が複雑なビジネスロジックを書きやすい(個人の感想)
- スケーラビリティ: 将来的にAPIだけスケールさせる可能性
認証フローは以下の分担:Keycloakがユーザー認証、NextAuthがOAuth/セッション管理、Go APIはKeycloakのアクセストークン(JWT)を検証して権限チェックを行う。
2. プロジェクト構成
saru/
├── apps/ # 6つのNext.jsアプリ
│ ├── system/ # System Portal (管理者)
│ ├── provider/ # Provider Portal (サービス提供者)
│ ├── reseller/ # Reseller Portal (販売代理)
│ ├── consumer/ # Consumer Portal (利用者)
│ ├── customer/ # Customer Portal (レガシー名、consumerと統合予定)
│ └── landing/ # ランディングページ
│
├── packages/ # 共有パッケージ
│ ├── types/ # TypeScript型定義
│ ├── ui/ # 共通UIコンポーネント
│ ├── api-client/ # APIクライアント + React Query hooks
│ ├── auth/ # NextAuth設定
│ ├── config/ # ESLint, TypeScript設定
│ └── env-validator/ # 環境変数バリデーション
│
├── backend/ # Go バックエンド
│ ├── cmd/
│ │ ├── system-api/ # System API (port 8080)
│ │ ├── provider-api/ # Provider API (port 8081)
│ │ ├── reseller-api/ # Reseller API (port 8082)
│ │ ├── consumer-api/ # Consumer API (port 8083)
│ │ └── migrate/ # マイグレーションCLI
│ └── internal/ # 共通ロジック
│
├── e2e/ # Playwright E2Eテスト
├── pnpm-workspace.yaml # pnpm workspace設定
└── turbo.json # Turborepo設定
なぜ4つのAPIに分けているのか
「1つのAPIで全部まかなえばいいのでは?」という疑問があるかもしれない。
分けた理由:
- 権限境界の明確化: System APIはシステム管理者のみ、Provider APIはプロバイダーのみがアクセス
- デプロイの独立性: Provider APIだけ更新したい時に他に影響しない
- コードの見通し: 1つのAPIに全エンドポイントがあると複雑になる
共通ロジックは internal/ で共有:
backend/internal/
├── domain/ # ドメインモデル
├── application/ # ユースケース
├── infrastructure/ # DB, 外部サービス
└── interfaces/ # ハンドラ、DTO
4つのAPIは同じ internal/ を参照し、必要なハンドラだけをルーターに登録する。
3. pnpm workspace + Turborepo
pnpm-workspace.yaml
| |
シンプル。apps/ と packages/ 配下がワークスペースになる。
turbo.json
| |
ポイント:
"dependsOn": ["^build"]— 依存パッケージを先にビルドdevはcache: false— 開発サーバーはキャッシュしないtype-checkは^buildに依存 — 型定義パッケージのビルドが先
よく使うコマンド
| |
4. 共有パッケージの設計
@repo/types — 型定義
| |
なぜ型だけ別パッケージにするのか:
- 複数のアプリで同じ型を使う
- バックエンドのレスポンス型と揃える
- 変更があれば全アプリに反映される
@repo/ui — 共通UIコンポーネント
| |
共通化の判断基準:
| 条件 | 配置先 |
|---|---|
| 2つ以上のポータルで同一実装 | packages/ui/ |
| 1つのポータル専用 | apps/[portal]/src/components/ |
| ポータル固有のビジネスロジック含む | 各アプリに配置 |
@repo/api-client — APIクライアント + React Query
| |
なぜhooksを共通化するのか:
- 同じAPIを叩くロジックを各アプリで書かない
- キャッシュ戦略を統一
- 型安全なAPI呼び出し
@repo/auth — NextAuth設定
| |
4つのポータルすべてで同じKeycloak設定を使う。
5. Go バックエンドの構成
4つのAPIエントリポイント
| |
| |
共通ロジックの共有
| |
どのAPIからも同じServiceを使う。違いはルーターで登録するエンドポイントだけ。
認可の多層防御
認可チェックの流れ:
1. Go API: JWTの署名検証 + アカウント種別チェック
2. Go API: ビジネスロジック層で操作権限チェック
3. PostgreSQL RLS: データアクセス時のテナント分離(最終防衛線)
API分割とRLSは別の責務を担う:
- API分割: エンドポイントレベルのアクセス制御(誰がどの機能を呼べるか)
- RLS: データレベルの分離(どのデータにアクセスできるか)
この多層防御により、仮にAPIの権限チェックに漏れがあっても、RLSが被害範囲を限定する(適切に設定されている前提)。
6. 型の共有:TypeScript ↔ Go
完全な自動同期はしていない。代わりに:
- OpenAPI仕様書を正として定義
- TypeScriptは
openapi-typescriptで生成 - Goは手動で合わせる(将来的には
oapi-codegen導入予定)
| |
正直な話: 完全自動化はできていない。Goの型とTypeScriptの型がズレることはある。E2Eテストで主要なフローは検知できるが、網羅的ではない。将来的にはOpenAPIスキーマのCIチェックで補完予定。
7. 開発サーバーの起動
| |
内部的には:
- Docker(PostgreSQL, Keycloak, Mailpit)を起動
- Go APIを4つ起動(air でホットリロード)
- Next.jsアプリを必要に応じて起動
| |
必要な環境変数
各ポータルで必要な主な環境変数:
| |
package.json スクリプト例
| |
8. ソロ開発で破綻しないために
やっていること
| 施策 | 効果 |
|---|---|
| 共通パッケージ化 | 同じコードを4箇所に書かない |
| Turborepo | ビルドキャッシュで高速化 |
| E2Eテスト | リグレッションを自動検知 |
| 統合スクリプト | 起動手順を覚えなくていい |
やらないこと
| 避けていること | 理由 |
|---|---|
| マイクロサービス化 | 4つのAPIはすでに十分複雑 |
| 過度な抽象化 | 3回以上重複してから共通化 |
| 自動型同期 | 設定コストが高い、E2E+将来のCIチェックで対応 |
まとめ
| 構成要素 | 採用技術 | ポイント |
|---|---|---|
| ワークスペース | pnpm workspace | apps/ + packages/ のシンプル構成 |
| ビルド | Turborepo | dependsOn で依存順序を管理 |
| 共通型 | @repo/types | 全アプリで型を共有 |
| 共通UI | @repo/ui | 2つ以上で使うコンポーネントを集約 |
| APIクライアント | @repo/api-client | React Query hooks を共通化 |
| 認証 | @repo/auth | NextAuth設定を共通化 |
| Backend | Go + Echo | 4 API、internal/ で共通ロジック共有 |
モノレポは初期設定のコストがあるが、一度構築すれば「あのアプリにも同じコンポーネントを追加して…」という作業が激減する。
ソロ開発で複数アプリを管理するなら、モノレポは強い選択肢だと思う。
シリーズ記事
- 第1回: 1人では保守できない複雑さに自動化で挑む
- 第2回: WebAuthn認証をCIで自動テスト
- 第3回: Next.js × Go モノレポ構成(本記事)