はじめに
みなさん「タコス」食べていますか?
こんにちは、tacosDB 開発責任者です。
タコスが好きすぎてタコスに特化したサイト「tacos DB」を開発しています。
既存のグルメサイトでは探しづらい、トルティーヤや具材などの条件でタコス店を探せるようにしたい、というのが開発のきっかけです。
前回は PR ごとにプレビュー環境が立ち上がる CI/CD 構成 について書きました。
今回は、前回の記事で少し触れた リポジトリ・ディレクトリ構成 についてまとめます。
tacos DB は、公開 Web、CMS、API、DB migration、インフラ定義まで同じリポジトリで管理しています。
ただ 1 つのリポジトリに全部を置くのではなく、モノレポの中で責務を明確に分ける構成にしています。
この記事では、AI と一緒に開発することを前提にした モノレポ時代のディレクトリアーキテクチャ について書きます。
技術スタック
| 領域 | 技術 |
|---|---|
| Backend | Rust / Cloudflare Workers |
| Frontend (Web) | Astro / React island |
| Frontend (CMS) | Next.js / OpenNext |
| Database | Cloudflare D1 |
| Storage | Cloudflare R2 |
| Infrastructure | Terraform / GitHub Actions |
| Agent support | AGENTS.md / repo-local skill |
全体のディレクトリ構成
現在の大枠は次のようになっています。
tacos/
├── AGENTS.md
├── DESIGN.md
├── README.md
├── Makefile
├── .agents/
│ └── skills/
├── blog/
│ └── tech/
├── spec/
├── tasks/
├── scripts/
├── src/
│ ├── rs/
│ ├── ts/
│ └── flutter/
├── terraform/
└── .github/workflows/
ルート直下には、プロジェクトの入口になるものだけを置いています。
| 場所 | 役割 |
|---|---|
README.md |
セットアップと主要コマンド |
AGENTS.md |
AI エージェント向けの作業ルール |
DESIGN.md |
UI デザイン方針の source of truth |
spec/ |
仕様、ER、API、画面、運用方針 |
tasks/ |
タスク定義と履歴 |
src/ |
実装 |
terraform/ |
Cloudflare resource 管理 |
.github/workflows/ |
CI/CD |
README は入口に寄せています。仕様や設計判断まで README に入れると、どの情報が正なのかが分かりづらくなるためです。
詳細は spec/ に置き、README から辿れる形にしています。
フロントエンドは Web と CMS を分ける
TypeScript 側は、公開 Web と CMS を別アプリにしています。
src/ts/
├── tacos-web/
└── tacos-cms/
| アプリ | 役割 | 採用技術 |
|---|---|---|
tacos-web |
店舗一覧、店舗詳細、エリアページ、Journal | Astro / React island |
tacos-cms |
店舗管理、画像管理、member 管理、Blog 記事管理 | Next.js / OpenNext |
公開 Web は検索流入と初期表示を重視します。店舗一覧や店舗詳細は、できるだけ軽く表示できることが重要です。そのため Astro を採用し、地図や絞り込みのような動的 UI だけ React island として実装しています。
CMS はログイン後の管理 UI です。フォーム、一覧、編集、画像 upload、権限管理が中心になります。そのため Next.js を採用しています。
同じ TypeScript でも、ユーザーと目的が違うためアプリを分けています。これにより、公開向けの UI と管理向けの UI が同じ components/ に混ざらないようにしています。
バックエンドは Rust Workspace でまとめる
バックエンドは src/rs/tacos-api-server にまとめています。
src/rs/tacos-api-server/
├── Cargo.toml
├── migrations/
├── wrangler.toml
├── wrangler.deploy.template.toml
├── wrangler.cms.deploy.template.toml
└── crates/
├── api/
│ ├── client-api/
│ └── cms-api/
└── modules/
├── item/
├── blog/
├── media/
├── member/
├── shared/
└── user/
大きく分けると、api と modules があります。
| ディレクトリ | 役割 |
|---|---|
crates/api/client-api |
公開 Web 向け Worker の HTTP entrypoint |
crates/api/cms-api |
CMS 向け Worker の HTTP entrypoint |
crates/modules/item |
店舗ドメイン |
crates/modules/blog |
Journal 記事ドメイン |
crates/modules/media |
画像 upload / R2 / metadata |
crates/modules/member |
member / session / RBAC |
crates/modules/shared |
本当に共有が必要な型や補助処理 |
api crate は HTTP の入口と DI に寄せています。業務ロジックは modules 側に置きます。
公開 API から呼ばれるか、CMS API から呼ばれるかは外側の都合です。店舗情報の整合性や公開状態の扱いは item module の責務、記事の公開や下書き管理は blog module の責務です。
module 内は 4 レイヤに揃える
各 domain module は、基本的に同じレイヤ構成にしています。
crates/modules/item/src/
├── domain/
│ ├── model/
│ ├── value/
│ ├── value_object/
│ └── repository.rs
├── usecase/
│ ├── input.rs
│ ├── output.rs
│ ├── query_service.rs
│ └── list_public_shops.rs
├── handler/
│ ├── request.rs
│ └── response.rs
└── infrastructure/
└── d1_repo.rs
| レイヤ | 役割 |
|---|---|
domain |
aggregate、entity、value object、repository trait |
usecase |
アプリケーションの操作、入力/出力 DTO、query service 契約 |
handler |
HTTP request の parse、response の render、error mapping |
infrastructure |
D1 / R2 / SQL / repository 実装 |
依存の流れは次のようにしています。
handler は HTTP の都合を扱います。usecase はアプリケーションの流れを扱います。domain は業務ルールを扱います。infrastructure は D1 や R2 など外部サービスの詳細を扱います。
この形にしておくと、AI に対しても「今回は usecase だけ」「SQL は infrastructure に置く」「domain に HTTP payload を入れない」のように指示できます。
module 間の境界を import ルールで守る
モジュラーモノリスでは、ディレクトリを分けるだけでは不十分です。
module 間で何でも直接 import できると、境界がすぐ曖昧になります。
tacos DB では、バックエンドの module 間通信について次のルールにしています。
- 同一 module 内は
handler -> usecase -> repository trait -> infrastructure - 同一 module 内の呼び出しに
clientを挟まない - 他 module の
usecaseを直接 import しない - 他 module と連携する場合は、公開された HTTP / client 契約を経由する
-
sharedは本当に複数 module で共有が必要になったものだけ置く
例として、blog module では記事の writer 情報を扱います。そのため将来的に member 情報が必要になります。
ただし、blog から member module の usecase を直接 import しない方針にしています。直接 import すると、blog が member の内部構造に依存してしまうためです。
また、Journal 記事から店舗を参照する機能を入れる場合は、blog から item への依存も発生します。この場合も item の usecase を直接 import するのではなく、公開された契約を通して連携する想定です。
module 間は契約でつなぎ、内部実装には触らない。これをモノレポの中でも守るようにしています。
DB は共有しても SQL は module に閉じる
tacos DB では Cloudflare D1 を使っています。
店舗、記事、画像、member などの table は同じ D1 に入っています。ただし、SQL の置き場所は module ごとの infrastructure に閉じています。
crates/modules/item/src/infrastructure/d1_repo.rs
crates/modules/blog/src/infrastructure/d1_repo.rs
crates/modules/media/src/infrastructure/d1_repo.rs
crates/modules/member/src/infrastructure/d1_repo.rs
同じ DB を使っていても、全 module が自由に全 table を触る構成にはしていません。
DB table を境界にしてしまうと、domain の言葉ではなく table の都合でコードを書くことになります。そのため、D1 row struct、SQL、repository 実装は infrastructure に置き、domain や usecase から SQL の詳細を見ないようにしています。
この分け方により、DB は共有しつつ、アプリケーション側の責務は module 単位で保てます。
shared は小さく保つ
モノレポでは、共通処理を shared に集めたくなります。
ただ、早い段階で共通化しすぎると、意味が違うものまで同じ型として扱ってしまいます。
例えば、同じ String でも ShopId と ArticleId は意味が違います。tacos DB では、意味を持つ識別子は value object として module 側に寄せています。
ShopId
ShopSlug
AreaId
AreaSlug
ArticleId
MediaImageId
shared は、複数 module が同じ契約を共有する必要が出たときだけ使います。
先に大きな共通層を作るのではなく、重複の意味が揃ってから共通化する方針です。
マイクロサービスよりモジュラーモノリス
ここまでが、tacos DB の Rust バックエンドの中身です。
crates/api に entrypoint を置き、crates/modules に domain module を置く。各 module の中は domain / usecase / handler / infrastructure に揃える。
この構成を採用している理由は、サービスを細かく分割したいからではありません。
tacos DB は個人開発のプロダクトです。公開 Web、CMS、API、DB、画像管理、デプロイまでありますが、今の規模でマイクロサービスに分ける必要はありません。
マイクロサービスにすると、サービス間通信、認証、デプロイ単位、監視、障害調査など、アプリケーション本体以外の設計・運用コストが大きくなります。
今の tacos DB で欲しいのは、サービス分割ではありません。
欲しいのは、次のような状態です。
- 1 つの PR で Web / CMS / API / DB / infra の変更を追える
- 1 つのリポジトリで仕様と実装をまとめて管理できる
- ローカル確認や CI の入口をそろえられる
- ただし、変更範囲や責務は曖昧にしない
そのため、リポジトリは 1 つにまとめています。
一方で、モノレポの中でアプリや domain module の境界は明確にします。サービスとしては分けないが、コード上の責務は分ける。これが今の tacos DB で採用しているモジュラーモノリスの位置づけです。
なぜ AI と相性がいいと思ったか
モジュラーモノリスにしているのは、AI と一緒に開発するときに相性が良いと感じているためです。
理由は大きく 3 つあります。
- 責務が明確になる
- レビューが容易になる
- AI への指示が簡単になる
AI エージェントに「店舗一覧 API を直して」と依頼したとき、item module があれば、まず見るべき場所を絞れます。
handler、usecase、domain、infrastructure が分かれていれば、どの層を触るべきかも伝えやすくなります。
レビューするときも同じです。
SQL が handler に出てきたら置き場所が違うと判断できます。HTTP response の都合が domain に入ってきたら、外側の事情が漏れていると判断できます。
人間にとって分かりやすい境界は、AI にとっても分かりやすい境界になります。
AI によってオーバーエンジニアリングのコストが変わった
以前なら、この規模の個人開発で Clean Architecture やモジュラーモノリスを導入するのは、少し重い判断だったと思います。
レイヤを分けると、ファイル数が増えます。DTO、repository trait、infrastructure 実装、request / response 変換なども必要になります。
手作業で毎回すべてを書くなら、オーバーエンジニアリングになりがちです。
ただ、AI エージェントを前提にすると、このコスト感が変わります。
AI は、決まった形の実装を高速に出すのが得意です。
たとえば次のような作業は、AI に任せやすいです。
- 既存 module と同じレイヤ構成で新しい usecase を追加する
- request DTO と response DTO を既存の命名に合わせる
- repository trait に method を追加し、D1 実装へ反映する
- 既存の handler / usecase / infrastructure の流れに沿って endpoint を増やす
- spec と実装の差分を見ながらレビューする
つまり、従来は導入コストが重く見えた構成でも、AI のアウトプット速度によって負担が下がります。
その結果、以前ならこの規模では採用しなかった設計でも、責務の明確化、レビューのしやすさ、AI への指示のしやすさといったメリットだけを受け取りやすくなったと感じています。
ここが、今回の記事で一番書きたいことです。
spec と tasks も構成の一部にする
ディレクトリ構成は src/ だけではありません。
tacos DB では、仕様と作業履歴もリポジトリ内で管理しています。
spec/
├── README.md
├── overview.md
├── project-structure.md
├── web-experience.md
└── tech/
├── item_er.md
├── blog_er.md
├── backend_coding_rules.md
├── screens/
├── sequences/
└── usecases/
tasks/
├── PROGRESS.md
└── 20260510235447-bl0g-add-qiita-blog-drafts.md
spec/ には仕様や設計判断を置きます。tasks/ にはタスクの履歴を残します。
コードだけでは、なぜその設計にしたのかが残りにくいです。例えば「他 module の usecase を直接 import しない」というルールは、実装だけでなく spec/tech/backend_coding_rules.md にも書いています。
完了したタスク本文は後から書き換えず、追加の変更があれば新しいタスクを切る運用にしています。
AI エージェント向けのルールも置く
tacos DB では、Codex などの AI エージェントを開発に使っています。
AI エージェントにとっても、ディレクトリ構成は重要です。どこに何があるか、どの層を触るべきかが明確だと、変更範囲を絞りやすくなります。
そのため、repo-local skill を .agents/skills/ に置いています。
.agents/skills/
├── tacos-backend-architecture/
│ └── SKILL.md
└── rust-programmer/
└── SKILL.md
tacos-backend-architecture には、このリポジトリ固有の module 境界や Clean Architecture のルールを書いています。
rust-programmer には、Rust 一般の API guideline、error handling、ownership、async などを書いています。
プロジェクト固有のルールと Rust 一般のルールを分けることで、AI エージェントに渡す前提も整理しやすくなりました。
やってよかったこと
この構成にしてよかった点は次のとおりです。
- README が入口として使いやすくなった
- 仕様の正本を
spec/から辿れるようになった - Worker crate を薄く保てるようになった
-
item/blog/media/memberの変更範囲を分けやすくなった - SQL や HTTP response の置き場所を判断しやすくなった
-
sharedへの過剰な共通化を避けやすくなった - AI エージェントに「どこを見て、どこを触らないか」を指示しやすくなった
一方で、最初からこの形だったわけではありません。
README に情報を集めすぎたり、将来用 module の意図が伝わりづらかったり、共通化の粒度で迷ったりしました。
そのたびに spec/ や AGENTS.md、repo-local skill にルールを落として、次から迷わないようにしています。
おわりに
この記事で書きたかったのは、モノレポと AI 開発は相性が良いのではないか、という話です。
ただし、1 つのリポジトリに全部を置くだけでは、変更範囲が広がりすぎます。
だからこそ、モノレポの中でディレクトリによる境界を作ります。
サービスの規模感としてはマイクロサービスにはしない。一方で、AI と一緒に開発するために、責務は明確に分ける。
今回紹介した方針をまとめると、次のようになります。
- README は入口に寄せる
- 仕様は
spec/に集める - 公開 Web と CMS は別アプリにする
- バックエンドは Rust Workspace にまとめる
- Worker crate は HTTP entrypoint と DI に寄せる
- 業務ロジックは
crates/modules/*に置く - module 内は
domain/usecase/handler/infrastructureに揃える - module 間は直接 usecase import しない
- DB は共有しても SQL は module の
infrastructureに閉じる -
sharedは小さく保つ - spec、tasks、AGENTS.md、repo-local skill まで含めて構成を設計する
以前なら小さなプロダクトには重く見えた構成でも、AI の実装速度によって導入コストは下がっています。
その結果、責務の明確化、レビューのしやすさ、AI への指示のしやすさというメリットを、小さな規模でも受け取れるようになってきたと感じています。
リリースした tacos DB は、まだ掲載店舗数を増やしている途中です。
店舗情報、写真、紹介文を提供してくれる方や、タコスが好きで一緒に整えてくれる方を募集しています。
知っているお店があれば、ぜひお問い合わせフォームから投稿してください。



