第20章 ユースケース入門②:入力・出力の形(DTO)📮📤
![hex_ts_study_020[(./picture/hex_ts_study_020_outbound_port_repository_inter.png)
この章は「ユースケースが、外の世界(CLI/HTTP/画面/DB)とどう会話するか」を、**DTO(Data Transfer Object)**でスッキリさせる回だよ〜😊✨ ここが決まると、あとで入口(CLI→HTTP)を差し替えても中心が揺れない💖
1) DTOってなに?ざっくり言うと「受け渡し用の伝票」🧾✨
DTOは、ユースケースが受け取る&返す**“ただのデータ”**だよ📦 ポイントはこれ👇
- ✅ 薄い(余計なメソッドや複雑さなし)
- ✅ 外に見せても安全(内部事情を隠せる🛡️)
- ✅ JSONにしやすい(HTTPと相性◎🌐)
たとえば Todo を「追加する」なら、
- 入力DTO:
{ title: "牛乳買う" }🥛 - 出力DTO:
{ todo: { id, title, completed } }✅
みたいな感じ😊
2) なぜ domain の型を外に漏らさないの?😱🧨
もしユースケースが Todo クラス(ドメイン)そのまま返すと、外側がこうなる👇
- 外側(HTTP/CLI)が Todoの内部構造に依存しちゃう
- ドメインを変更したら、外側も巻き込まれて壊れがち💥
- JSON化で想定外の形になったり、テストが読みづらくなったり😵💫
DTOにしておくと…
- 中心(ドメイン)の変更が外に漏れにくい🛡️
- 入口(CLI→HTTP)を変えてもユースケースが安定する🔁✨
- テストが「仕様の文章」みたいに読める🧪📖
3) DTOの設計ルール(これだけ守ればOK)✅✨
DTOは「外へ渡す前提」だから、だいたいこうするのが安全だよ👇
✅ ルールA:基本はプリミティブ中心(string/number/boolean)🧱
Dateは避けて ISO文字列にしよ("2026-01-23T...")🕒- クラスインスタンスは渡さない🙅♀️
✅ ルールB:DTOは “ただの箱” 📦
- メソッドなし
- ロジックなし(判断や状態遷移はドメインへ🧠)
✅ ルールC:名前は「外に見せたい言葉」💬
- ドメインの内部用語(濃い言葉)をそのまま出さなくていい🙆♀️
4) TodoミニアプリのDTOを作ろう📝🍰
フォルダ例(迷子防止)📁🧭
src/app/dtos/… DTO置き場📮src/app/usecases/… ユースケース🎮src/domain/… ルール(ドメイン)🧠
まずは DTO を作るよ✨
// src/app/dtos/todo.dto.ts
export type TodoDto = Readonly<{
id: string;
title: string;
completed: boolean;
}>;
export type AddTodoInputDto = Readonly<{
title: string;
}>;
export type AddTodoOutputDto = Readonly<{
todo: TodoDto;
}>;
export type CompleteTodoInputDto = Readonly<{
id: string;
}>;
export type CompleteTodoOutputDto = Readonly<{
todo: TodoDto;
}>;
export type ListTodosOutputDto = Readonly<{
todos: readonly TodoDto[];
}>;
Readonly を付けるのは「外に渡すデータは基本いじらない」って気持ちになるからおすすめ😊🛡️
5) ドメイン → DTO への変換(Mapper)🔁🧩
ここが超大事✨ **変換は中心じゃなくて“アプリ層(ユースケース寄り)”か“アダプタ”**でやるとキレイだよ🧼
例:Todo ドメインがクラスだとして…
// src/app/dtos/todo.mapper.ts
import { Todo } from "../../domain/todo/Todo.js";
import { TodoDto } from "./todo.dto.js";
export function toTodoDto(todo: Todo): TodoDto {
return {
id: todo.id, // getterでもOK
title: todo.title,
completed: todo.completed,
};
}
✅ “外に渡す形”は、この
toTodoDtoが責任もつ感じ💖
6) ユースケースの「入口と出口」をDTOで固定する🚪📤✨
ユースケースは、外側の都合(HTTPのRequestとか)を知らない🙅♀️ なので、DTOだけ知ってればOKにするよ!
// src/app/usecases/AddTodoUseCase.ts
import { AddTodoInputDto, AddTodoOutputDto } from "../dtos/todo.dto.js";
import { toTodoDto } from "../dtos/todo.mapper.js";
import { Todo } from "../../domain/todo/Todo.js";
import { TodoRepositoryPort } from "../ports/TodoRepositoryPort.js";
export class AddTodoUseCase {
constructor(private readonly repo: TodoRepositoryPort) {}
async execute(input: AddTodoInputDto): Promise<AddTodoOutputDto> {
// 入力チェック(軽いのはここでもOKだけど、方針は後の章で整理するよ😊)
const todo = Todo.create(input.title);
await this.repo.save(todo);
return { todo: toTodoDto(todo) };
}
}
ここでの気持ち👉
- 入力はDTO(外から来る伝票📮)
- 内部はドメイン(ルールで作る🧠)
- 出力はDTO(外へ返す伝票📤)
7) DTOを使うとテストがめっちゃ読みやすい🧪✨
返ってくるのが DTO だと、テストがこうなる👇(読みやすい〜!😍)
const out = await addTodo.execute({ title: "牛乳買う" });
expect(out.todo.title).toBe("牛乳買う");
expect(out.todo.completed).toBe(false);
ドメインの複雑さ(メソッドとか)をテストが知らなくて済むのが最高💖
8) 「DTOが薄いか」セルフチェック✅🥗
DTOが太り始めると事故るので、これで点検しよ〜🚨
- ❌ DTOに
validate()とかcomplete()とかメソッド生えてる - ❌ DTOの中にドメインクラス入ってる(
todo: Todoとか) - ❌ DTOがDBの形そのまま(
created_atとか外部都合が混入) - ✅ DTOは “外に見せたい言葉” だけになってる
- ✅ JSONにしても自然な形になってる
9) AIに頼むならこの聞き方が安全🤖📝✨
雛形生成(OK)🧰
TodoアプリのUseCase用DTOを作りたいです。
AddTodo / CompleteTodo / ListTodos の input/output DTO を
「プリミティブ中心」「Readonly」「JSON化しやすい」形でTypeScriptで提案して。
レビュー(めっちゃおすすめ)🔍
このDTO設計で「ドメインが外に漏れてないか」「DTOが太ってないか」
「将来HTTP/CLIに差し替えても中心が変わらないか」を観点にチェックして。
10) 最新のTSまわり小ネタ(DTO設計がラクになるやつ)✨
-
npm の
typescript最新は 5.9.3(2026年1月時点)だよ📌 (NPM) -
TypeScript 5.9 では、Node向けの設定として
--module node20みたいな「安定ターゲット」も用意されてるよ(挙動がブレにくい方針)🧭 (typescriptlang.org) -
DTOの入力検証をライブラリでやるなら、Zod は v4系が安定していて(例:v4.3.0)、DTOと相性がいいよ✅ (GitHub)
- 軽量派なら Valibot も選択肢だよ🪶 (valibot.dev)
(※検証の置き場所は次の章以降で「境界でやる」方針に寄せていくよ😊)
11) ミニ課題🎀📝(5〜10分でOK)
課題A:ListTodosOutputDto を整える✨
todosをreadonly配列にしたままTodoDtoの形が崩れないことを確認してね✅
課題B:将来用フィールドを入れてみる🕒
createdAt: string(ISO文字列)をTodoDtoに追加- ドメインの Date をそのまま出さないことがポイントだよ🙅♀️💡
この章のまとめ🎁💖
- DTOは「伝票」📮📤
- ユースケースの入口と出口をDTOで固定すると、中心が守れる🛡️
- ドメインは濃くてOK、外に出すときはDTOで薄くする🥗✨
- 変換(Mapper)を1か所に集めると、未来がラク🔁💕
次はこのDTOが「Port(約束)」にどう繋がっていくかが気持ちいいところだよ〜🔌✨