第11章:Application入門② DTOと境界変換(Mapping)🧩📦
この章でできるようになること🎯✨
- **DTO(入出力の箱)とDomainモデル(ルールの宝石箱💎)**を混ぜずに扱える😊
- **「どこで変換するか?」**が迷わなくなる🚪🧭
- 変換だらけで迷子にならない整理術が身につく🧹✨
- 「後でAPIや画面が変わっても、Domainが死なない」構造にできる🛡️💪
1) DTOってなに?🍱(超ざっくり)
DTOは Data Transfer Object の略で、ひとことで言うと…
外の世界(画面/HTTP/DB/外部API)とデータをやり取りするための“運搬用の箱”📦
- 入力DTO:フォームやHTTPで受け取る“入力の箱”📥
- 出力DTO:画面やAPIレスポンスへ返す“出力の箱”📤
ここで大事なのは👇 DTOは “都合の箱” であって、ルールの本体(Domain)ではないってこと🙂✨
2) なんでDomainと分けるの?💥(混ぜると事故る)
DTOとDomainを混ぜると、こういう事故が起きがち😵💫
事故あるある①:画面都合がDomainに侵入する🧟♀️
- 例:フォームが
titleじゃなくてtodo_titleになった → Domainのプロパティ名まで変える羽目に💥
事故あるある②:入力が文字列だらけ問題🧵
- HTTP入力はだいたい string で来る(数値でも日付でも!)
→ Domainが
stringまみれになってルールが崩壊😇
事故あるある③:テストがしんどい🧪💦
- DomainがHTTPや画面の形に寄るほど、テストが“外部依存”になる → Domainが純粋じゃなくなる=ご褒美(テストの簡単さ🍰)が消える
3) “箱は3つ”で考えるとラク🧠✨

この章の結論はこれ👇
**入力DTO(外)**📥 → **Domain(核)**💎 → **出力DTO(外)**📤
イメージ図🗺️✨(超重要!)
Presentation(HTTP/画面) 🎛️
↓ 入力DTO 📥(外の形)
Application(UseCase) 🎮
↓ Domainへ変換 💎(ルールの形)
Domain(ルール) 💎
↑ Domainから変換 📦(表示の形)
Application(UseCase) 🎮
↑ 出力DTO 📤
Presentation(HTTP/画面) 🎛️
4) 「境界で変換する」ってどこ?🚪🧩
“境界”っていうのは、だいたいこの2つの境目だよ🙂
✅ 境界①:Presentation → Application(入口)🚪📥
- HTTP/画面イベントの都合をここで吸収する
- **UseCaseが欲しい形(入力DTO)**にして渡す
✅ 境界②:Application → Presentation(出口)🚪📤
- Domainの結果を、画面/レスポンス都合の形に整える
- 出力DTOにして返す
👉 Domain層はDTOを知らない(importしない) これが一生モノのルール🛡️✨
5) 実例:ToDo追加(最短でわかるやつ)🧵📝
想定する要件(超ミニ)🌱
- 入力:
title(空はダメ🙅♀️) - 出力:作成したToDoの
id,title,done
5-1. フォルダ配置(迷子防止)🗂️✨
UseCase単位でまとまってると最高にラク😊
-
src/application/usecases/createTodo/CreateTodoCommand.ts(入力DTO)📥CreateTodoResult.ts(出力DTO)📤createTodo.ts(ユースケース本体)🎮mappers.ts(変換をここに集約)🧩
-
src/domain/Todo.ts(Entity)🪪TodoTitle.ts(VO)🔒
-
src/presentation/http/createTodoHandler.ts(HTTPの薄い層)🎛️
5-2. 入力DTO(Command)📥
// src/application/usecases/createTodo/CreateTodoCommand.ts
export type CreateTodoCommand = {
title: string; // HTTPでは文字列で来る前提
};
5-3. 出力DTO(Result)📤
// src/application/usecases/createTodo/CreateTodoResult.ts
export type CreateTodoResult = {
id: string;
title: string;
done: boolean;
};
5-4. Domain(例:Titleは空禁止🔒)💎
// src/domain/TodoTitle.ts
export class TodoTitle {
private constructor(public readonly value: string) {}
static create(raw: string): TodoTitle {
const v = raw.trim();
if (v.length === 0) throw new Error("TITLE_EMPTY");
if (v.length > 50) throw new Error("TITLE_TOO_LONG");
return new TodoTitle(v);
}
}
5-5. 変換(Mapping)を“境界”に集める🧩🚪
ポイントは 副作用なし(pure) ✨ 「変換するだけ」「DB触らない」「HTTP知らない」👍
// src/application/usecases/createTodo/mappers.ts
import { TodoTitle } from "@/domain/TodoTitle";
import type { CreateTodoCommand } from "./CreateTodoCommand";
import type { CreateTodoResult } from "./CreateTodoResult";
import type { Todo } from "@/domain/Todo";
export function toDomainTitle(cmd: CreateTodoCommand): TodoTitle {
return TodoTitle.create(cmd.title);
}
export function toResult(todo: Todo): CreateTodoResult {
return {
id: todo.id.value,
title: todo.title.value,
done: todo.done,
};
}
5-6. UseCase本体(Application)🎮
UseCaseは「手順書」📋 Domainを呼んで、結果をDTOにして返すだけに寄せる😊
// src/application/usecases/createTodo/createTodo.ts
import type { CreateTodoCommand } from "./CreateTodoCommand";
import type { CreateTodoResult } from "./CreateTodoResult";
import { toDomainTitle, toResult } from "./mappers";
import { Todo } from "@/domain/Todo";
import type { TodoRepository } from "@/application/ports/TodoRepository";
export async function createTodo(
repo: TodoRepository,
cmd: CreateTodoCommand
): Promise<CreateTodoResult> {
const title = toDomainTitle(cmd); // DTO → Domain
const todo = Todo.createNew(title); // Domainルールで生成💎
await repo.save(todo); // 外側はPort経由🔌
return toResult(todo); // Domain → DTO
}
6) 入力は“型だけ”じゃ守れない問題🛡️(2026の定番)
HTTPやフォームの入力は unknown と思って扱うのが安全😊 そこでよく使われるのが スキーマバリデーション(実行時チェック)✨
- Zod:v4系が安定版になっていて、npmの最新は 4.3.5(2026-01時点) (Zod)
- Valibot:小さくてモジュラー設計、npmの最新は 1.2.0(2025-11時点) (Valibot)
(どっちが正解!ではなく、プロジェクトの好みでOKだよ😊)
6-1. Presentation側で「unknown→入力DTO」へ📥🚪
例としてZodでやってみるね✨(入口で弾くのが気持ちいい🧼)
// src/presentation/http/createTodoHandler.ts
import { z } from "zod";
import { createTodo } from "@/application/usecases/createTodo/createTodo";
import type { TodoRepository } from "@/application/ports/TodoRepository";
const CreateTodoSchema = z.object({
title: z.string().min(1).max(50),
});
export async function createTodoHandler(
repo: TodoRepository,
body: unknown
) {
const parsed = CreateTodoSchema.parse(body); // unknown → 安全な形へ🛡️
// ここで “入力DTO” にする(Application入口へ)📥
const cmd = { title: parsed.title };
const result = await createTodo(repo, cmd);
// resultは出力DTOなので、そのままレスポンスへ📤
return { status: 201, json: result };
}
※ Zodは「TypeScriptの型は実行されない」問題を補ってくれる系の代表例だよ😊 (npm)
7) 「変換だらけで迷子」防止の整理術🧹🧭
ここ、超大事!!✨ 迷子になる人めっちゃ多い😵💫
✅ ルール1:DTOは“ユースケース単位”で作る🎮📦
CreateTodoCommand/CreateTodoResultみたいに- でっかい共通DTOを作ると、だいたい腐る🍂
✅ ルール2:Mapperは“ユースケースの近く”に置く🧩
- 変換が散らばると追えない
- 1ユースケースに
mappers.ts1枚、めっちゃおすすめ😊
✅ ルール3:DomainはDTOをimportしない🙅♀️
- 逆に「DTOがDomainをimport」するのもなるべく避けたい
- 依存の向きが綺麗なほど勝ち🏆✨
✅ ルール4:変換は“副作用なし”にする🧼
- DBアクセスしない
- 時刻取得しない
- ネットワーク呼ばない → ただの変換にするとテストが秒で終わる🧪⚡
✅ ルール5:名前を統一する(地味に最強)🏷️
- 入力:
XxxCommand/XxxQuery - 出力:
XxxResult/XxxView - 変換:
toDomainXxx,toResult,toDto命名が揃うと迷子率が激減😊✨
8) よくある失敗と直し方🙈➡️😊
❌ 失敗1:UseCaseがHTTPの形を直接受け取る
createTodo(repo, req.body)とか ✅ 直し:Presentationでunknown→入力DTOにしてから渡す📥
❌ 失敗2:Domainに表示用フィールドが混ざる
TodoにdisplayTitleとか入れちゃう ✅ 直し:それは出力DTO側で作る📤(整形は外)
❌ 失敗3:Mapperが巨大化して神クラス化👼💥
✅ 直し:
- “ユースケース単位で分割”
- “VO生成は関数に切り出し”
- “DTOを小さくする”
9) ミニ演習🧩✍️(30〜45分くらいの手触り)
題材:ToDoに「締切(deadline)」を追加📅✨(でもDomainは日付ルール守る)
やること
- 入力DTOに
deadline?: stringを追加(HTTPは文字列)📥 - Domainに
DeadlineVOを作る(過去日はNG🙅♀️ とか)💎 - Mapperで
string → Deadlineへ変換🧩 - 出力DTOは
deadline?: stringで返す(ISO文字列)📤 - 「deadline無し」も通るようにする😊
ゴール✅
- Domainが string日付を知らない
- Presentationが unknownを信用してない
- 変換が “境界にまとまってる” 🧹✨
10) AI活用プロンプト集🤖💡(そのままコピペOK)
- 「この
CreateTodoCommandを見て、Domainに持ち込むべきでない項目があるかレビューして🙏」 - 「DTO→Domain→DTO の変換が増えた時、フォルダ/命名の整理案を3パターン出して😊」
- 「
mappers.tsが肥大化しそう。責務分割の切り方を提案して!」 - 「入力の
unknownを安全にするためのZod/Valibotスキーマ案を作って✨」 (Valibot)
章末チェック✅🌟
- DTOとDomainを“別物”として説明できる🙂?
- 入口(Presentation→Application)で
unknownを弾ける🛡️? - DomainがDTOをimportしてない🙅♀️?
- Mapperが“副作用なし”になってる🧼?
- 「変換は境界に置く」が自分の言葉で言える🚪🧭?
おまけ:2026っぽい最新トピック小話📣✨
- TypeScriptの最新安定版は 5.9系(公式ダウンロードページでも “currently 5.9” と案内) (TypeScript)
- さらに将来に向けて、コンパイラのネイティブ化(TypeScript 7 Native Preview)も進んでるよ⚡(ビルド高速化が大きなテーマ) (Microsoft Developer)
次の第12章は、ここで出てきた TodoRepository みたいな Port(interface)で依存を逆転していくよ🔌➡️✨
その前に、もし今の章の題材を「読書ログ📚」とか「推し活支出メモ💸」に寄せた例も欲しかったら、同じ構造でサクッと作るね😊💕