第8章 Composition Root入門①:組み立て場所(entry)を決める🏗️📌
この章のゴール🎯✨
- 「実装をどこで選ぶか?」を 1か所に固定できるようになる😊
domain/appが 外側(DB/HTTP/SDK)を知らない状態をキープできるようになる🧅🛡️main.ts(またはserver.ts)に 組み立て(配線)を集約して、変更に強い形にする💪🔌
1) Composition Rootってなに?🧩✨

**Composition Root(コンポジションルート)**は、ざっくり言うと…
アプリの「入口」にある、依存(オブジェクトたち)をぜんぶ組み立てる場所📍
「どの実装を使うか(例:DB版Repo or InMemory版Repo)」を決めて、必要なものを作って、つないで、アプリを起動する…その担当だよ〜!🚀 定義としても「アプリのエントリーポイント付近で、オブジェクトグラフ全体を組み立てる場所」と説明されることが多いよ📌 (InfoQ)
2) なんで必要なの?(ないと何が困る?)😵💫💥
Composition Rootがない(=いろんな場所で new してる)と、こんな事故が起きがち👇
appの中でnew SqliteTodoRepository()とかし始める → 中心が外側を知っちゃう(Dependency Rule違反)🚨🧅- あちこちで生成してるから「差し替え」が地獄になる → テストで InMemory に変えたいのに、探して直して…つらい😭🔁
- 設定(開発/本番)で実装を切り替えたいのに、散らばってて壊れやすい → “変更の波及” が増える😱🔗
だから「作る場所を1か所」にして、選択と配線をまとめるんだ〜!🏗️✨
3) Composition Rootはどこに置く?📁📌
基本はこのどれかでOKだよ😊
src/main.ts(CLIっぽい起動・バッチも含め万能)🧠src/server.ts(サーバー起動が主役ならコレ)🌐src/index.ts(ただし barrel と混同しやすいので注意⚠️)
そしてルールは超シンプル👇
✅ Composition Rootだけが “外側の実装” をimportしていい ✅ それ以外(domain/app)は 契約(interface/type)だけを見る👀✨
「DIコンテナを使うなら、コンテナ参照もComposition Rootだけ」って考え方もよく言われるよ📦(この教材では次章で扱うね) (Stack Overflow)
4) まずは “手動DI” で十分😌🪶
この章では DIコンテナなしでいくよ! やることはこれだけ👇
domain:型・ルール・エラー(純粋に保つ)🧼app:ユースケース(必要な契約だけ受け取る)🧩adapters:外部I/O実装(DB/HTTP/ファイル等)🔌main.ts:全部を作って、つないで、起動する🏗️🚀
5) ミニ題材:ToDoを “差し替え可能” にして起動する📝✨
5-1. フォルダ構成(地図)🗺️📁
こんな感じでいこう(最小でOK!)😊
src/domain/… ルールと契約🧅src/app/… ユースケース🧠src/adapters/… 実装(外側)🔌src/main.ts… 組み立て&起動🏗️
6) コードで体験しよ〜!👩💻✨(Composition Rootの本質)
6-1. domain:契約(Repository)と型📜
// src/domain/todo.ts
export type Todo = {
id: string;
title: string;
done: boolean;
};
// src/domain/todoRepository.ts
import type { Todo } from "./todo";
export interface TodoRepository {
add(todo: Todo): Promise<void>;
list(): Promise<Todo[]>;
}
6-2. app:ユースケース(契約だけ見てる!)🧠🧩
// src/app/addTodo.ts
import type { TodoRepository } from "../domain/todoRepository";
import type { Todo } from "../domain/todo";
export type AddTodoInput = { title: string };
export function makeAddTodo(deps: { repo: TodoRepository }) {
return async (input: AddTodoInput): Promise<Todo> => {
const todo: Todo = {
id: crypto.randomUUID(),
title: input.title.trim(),
done: false,
};
await deps.repo.add(todo);
return todo;
};
}
// src/app/listTodos.ts
import type { TodoRepository } from "../domain/todoRepository";
export function makeListTodos(deps: { repo: TodoRepository }) {
return async () => deps.repo.list();
}
ポイント✨
app は repoの実装(DBとか)を一切知らないよね?最高〜!🧅💕
6-3. adapters:外側の実装(例:InMemory Repo)🧰
// src/adapters/inMemoryTodoRepository.ts
import type { TodoRepository } from "../domain/todoRepository";
import type { Todo } from "../domain/todo";
export class InMemoryTodoRepository implements TodoRepository {
private todos: Todo[] = [];
async add(todo: Todo): Promise<void> {
this.todos.push(todo);
}
async list(): Promise<Todo[]> {
return [...this.todos];
}
}
6-4. adapters:HTTP(超ミニのNode http)🌐✨
フレームワークを増やさず、Composition Rootの説明に集中するために “最小” でいくね😊
// src/adapters/httpServer.ts
import http from "node:http";
export function createHttpServer(deps: {
addTodo: (input: { title: string }) => Promise<unknown>;
listTodos: () => Promise<unknown>;
}) {
return http.createServer(async (req, res) => {
try {
const url = new URL(req.url ?? "/", "http://localhost");
// GET /todos
if (req.method === "GET" && url.pathname === "/todos") {
const todos = await deps.listTodos();
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify(todos));
return;
}
// POST /todos
if (req.method === "POST" && url.pathname === "/todos") {
let body = "";
req.on("data", (chunk) => (body += chunk));
await new Promise<void>((resolve) => req.on("end", () => resolve()));
const parsed = JSON.parse(body || "{}") as { title?: string };
const created = await deps.addTodo({ title: parsed.title ?? "" });
res.writeHead(201, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify(created));
return;
}
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
res.end("Not Found");
} catch (e) {
res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
res.end("Server Error");
}
});
}
7) ここが本題!main.ts(Composition Root)🏗️📌✨
「作る・選ぶ・つなぐ・起動する」は全部ここ!💪
// src/main.ts
import { InMemoryTodoRepository } from "./adapters/inMemoryTodoRepository";
import { createHttpServer } from "./adapters/httpServer";
import { makeAddTodo } from "./app/addTodo";
import { makeListTodos } from "./app/listTodos";
function createApp() {
// 1) 実装を選ぶ(ここが“差し替えポイント”)🔁✨
const repo = new InMemoryTodoRepository();
// 2) ユースケースを組み立てる(契約を注入)🧩
const addTodo = makeAddTodo({ repo });
const listTodos = makeListTodos({ repo });
// 3) 外側(HTTP)にユースケースを渡す🌐
const server = createHttpServer({ addTodo, listTodos });
return { server };
}
const { server } = createApp();
server.listen(3000, () => {
console.log("http://localhost:3000 で起動したよ〜🚀✨");
});
ここが超大事ポイント💡🧠
domain/appは 外側をimportしてない(守れてる!)🧅✨main.tsは 外側をimportしてOK(ここが例外の場所)🏗️- 「差し替え」は
repo = ...の1行で済む未来が見える👀🔁
8) “オブジェクトグラフ”ってなに?🕸️✨
雑に絵にするとこんな感じだよ👇
main.tsが全部を作るappは契約(repo)を受け取るadaptersは app を呼ぶ
つまり「つながりの全体図(グラフ)」を 入口で組み立てるのが Composition Root! この考え方は “入口で全体を組む場所” として紹介されてるよ📌 (InfoQ)
9) よくある失敗あるある😇⚠️(ここで直せるように!)
❌ 失敗1:ユースケース内で実装を作っちゃう
// ダメ例:app内で実装new(外側を知っちゃう)
const repo = new InMemoryTodoRepository();
✅ 直し方:main.ts で作って、makeAddTodo({ repo }) で渡す!🏗️✨
❌ 失敗2:設定(env)を中心に漏らしちゃう
// ダメ例:app内で process.env を参照
if (process.env.NODE_ENV === "production") { ... }
✅ 直し方:main.ts で判断して “選んだもの” を渡す😊🔁
10) ミニ演習👩💻📝(10〜20分でOK✨)
演習A:Repoを2種類にして切り替え🔁
InMemoryTodoRepositoryに加えてFileTodoRepository(ファイル保存)を作った体でOK!📁main.tsの1か所だけで切り替えられるようにしてみてね✨
ヒント:main.ts にこういう “選択行” を作るイメージ👇
(※実装は次章以降で重くしてもOK!)
const useMemory = process.env.TODO_REPO === "memory";
const repo = useMemory ? new InMemoryTodoRepository() : new FileTodoRepository("todos.json");
Node.jsは2026-01-13に 24.13.0 (LTS) のセキュリティリリースが出てるので、こういう環境変数運用+更新は意識しておくと安心だよ🛡️✨ (Node.js)
演習B:「new が散らばってる場所」をmainへ集約🏗️
- プロジェクト内で
newを検索してみて - “中心っぽい場所” に
newがいたら、main.tsにお引っ越しさせる🧳✨
11) AIに手伝ってもらうプロンプト例🤖🪄
そのままコピペで使えるやつ置いとくね💕
- 「このプロジェクト構成で
main.tsにComposition Rootを作りたい。repoを注入してcreateApp()を組むテンプレを作って」🧩 - 「
appがadaptersをimportしてる箇所があれば指摘して、main.ts経由の注入に直して」🕵️♀️ - 「依存の向きがわかるように、import関係を箇条書き+簡易図にして」🗺️
12) 章末チェック✅🌸(できたら勝ち!)
-
domain/appが外側(HTTP/DB/FS)をimportしてない🧅🚫 - “実装の選択” が
main.tsに1か所で集まってる🔁📌 -
createApp()みたいに「組み立て」が見える形になってる🏗️✨ - テスト用に InMemory を差し替えられそうな匂いがする🧪💕
おまけ:ツール事情(2026の空気だけ)🌤️
TypeScriptは「ネイティブ実装(高速化)」の流れが強くて、開発体験がどんどん良くなってるよ⚡(コンパイル速度など) ただ、Composition Rootの考え方はツールが変わっても超安定なので安心してOK😊✨ (Microsoft Developer)
次の第9章では、この “手動DIの配線” を パターン化して、もっとスッキリ回す方法(DIコンテナ無しでの型)をやるよ〜🪶🧪✨