Contents
なぜ「型設計」が崩れるのか:DTOとドメインモデルが混ざる瞬間
開発現場でよく起きるのが、「最初は綺麗だったTypeScriptの型が、いつの間にか“なんでも入る箱”になっていく」問題です。特にNext.jsのように画面・API・サーバー処理が同じリポジトリに並ぶ構成では、データの通り道が短く見えるぶん、設計の境界線が曖昧になりやすいです。
崩れ始める典型は、「APIで返ってきたJSONの形」をそのままアプリ全体の“正解”として扱ってしまうことです。例えば、顧客情報を取得するAPIのレスポンスに createdAt が文字列で入っていたり、ステータスが "0" / "1" のような文字列だったりしても、画面表示ができるうちはそのまま進めてしまいます。ところが後から「計算」「集計」「承認フロー」など業務要件が増えた瞬間、型の揺れが一斉にバグへ変わります。
ここで重要なのが、DTO(外部との受け渡し用の型)と、ドメインモデル(社内の業務ルールを守る型)を分ける考え方です。DTOは「外から来る・外へ出すデータの形」であり、現実には欠損や表記ゆれ、過去互換、ベンダー都合が混ざります。一方ドメインモデルは「自社の業務として正しい状態」を表します。両者を混ぜると、社内の正しさが外部都合に引きずられ、型設計が崩れていきます。
非エンジニアの方に例えるなら、DTOは「取引先から届く注文書のフォーマット」、ドメインモデルは「社内の基幹システムに登録する正式な受注データ」です。注文書の書き方は相手都合で揺れますが、社内登録のルールは揺らすと混乱します。“受け取った形”と“社内の正”を同じにしないのが、長期運用で効く基本です。
3分でできる! 開発費用のカンタン概算見積もりはこちら
DTOとドメインモデルの役割:責任範囲を分けると運用コストが下がる
DTO(Data Transfer Object)は、ネットワーク越し・画面入力・外部サービス連携など「境界」を跨ぐときの器です。DTOに期待するのは、相手が送ってきたものを安全に受け取れること、そして必要なら「相手が求める形式で返せること」です。つまりDTOは“現実の揺れ”を受け止めるクッションで、多少の汚さを許容します。たとえば email が空文字でも来る、日付が文字列、数値が文字列で来る、といった事態は起こり得ます。
一方ドメインモデルは、自社の業務ルールに沿ったデータです。例えば「請求書は必ず発行日が必要」「ステータスは定義された値のみ」「金額は負の値にならない」など、運用ルールを体現します。ここが曖昧だと、画面の見た目は整っても、月末の締め処理や権限管理、監査対応で破綻します。ドメインモデルは“守るべき正しさ”を表すと覚えると理解しやすいです。
Next.jsのプロジェクトでは、特に以下の境界が混ざりやすいです。
- フォーム(ユーザー入力)→ サーバーアクション/API → DB保存
- 外部SaaS(会計・CRM)→ 自社API → 画面
- DBの行(テーブル構造)→ 画面表示用のデータ
これらを全部「同じTypeScript型」で通すと、変更に弱い構造になります。例えば、会計SaaSの都合でレスポンス項目が増減しただけで、社内の型に影響が波及します。逆に、社内で承認ステータスを増やしただけで、外部連携のDTOまで壊します。そこで、DTO・ドメイン・永続化(DB)・表示(ViewModel)の責任を分離すると、影響範囲が小さくなり、修正が速くなります。
予算とスピードの観点でも、最初に境界を分ける設計は有利です。短期的には型が増えるため「回り道」に見えますが、運用開始後に発生する仕様変更・例外対応の工数が大きく減ります。情シスや管理職の立場なら、「小改修が積み重なるほど差が出る」投資だと考えると判断しやすいはずです。
Next.jsでの実装イメージ:API/Server Actions/DBで境界を作る
Next.jsでは、API Routes(/api)やRoute Handlers、Server Actions、そしてDBアクセス(Prisma等)といった層が同居します。ここでおすすめなのは、「層ごとに型の置き場所を分ける」ことです。配置ルールがあるだけで、DTOとドメインが混ざりにくくなります。
一例として、次のようなディレクトリ分割が現実的です。
src/
domain/
customer/
Customer.ts
CustomerStatus.ts
CustomerService.ts
application/
customer/
registerCustomer.ts
infrastructure/
db/
prismaCustomerRepository.ts
presentation/
api/
customers/route.ts
forms/
customerFormSchema.ts
shared/
errors/
types/
ポイントは、presentation(外側)にDTOや入力検証、domain(内側)に業務ルールを置くことです。APIのroute.tsやServer Actionsは「受け取る」「返す」に責任を持ち、ドメインモデルへ変換してから内部処理へ渡します。DB行(Prismaの型)はinfrastructureに閉じ込め、ドメインをPrismaの都合に引っ張られないようにします。
APIの受け取りは、DTOを“厳密に”するよりも、まず「壊れても落ちない」受け方が重要です。例えばフォーム入力は、空欄や全角混在、想定外の文字が入ります。そこで、DTO(入力)→ バリデーション → 正規化 → ドメイン生成、の順にして、ドメインに入る前に整えます。ドメインモデルは「整った世界だけ」を扱う設計にすると、内部処理がシンプルになります。
またNext.jsの“便利さ”として、クライアントとサーバーの境界が見えにくい点があります。Server Actionsでフォームを直接処理できるため、DTOを作らずにそのままドメインへ…と進みがちです。しかし運用上は、入力スキーマ(DTO相当)を置き、エラー表示・ログ・監査に耐える形にしておく方が安全です。例えば「どの項目が不正だったか」を返すためには、ドメインの例外よりもDTO検証の結果が必要になります。
3分でできる! 開発費用のカンタン概算見積もりはこちら
具体例:顧客登録で「DTO→変換→ドメイン」を通す(TypeScript)
ここでは「顧客登録」を例に、DTOとドメインモデルの分け方をイメージできるようにします。なお、コードは概念説明用で、実プロジェクトではエラーハンドリングやログ、権限チェックを追加します。
まず外部(フォームやAPI)から受け取るDTOは、“来てしまう可能性がある形”を表します。日付が文字列、数値が文字列、任意項目が未入力などを想定します。
export type CustomerCreateDTO = {
name?: unknown;
email?: unknown;
status?: unknown; // "active" | "inactive" を期待するが、現実には何でも来る
startDate?: unknown; // "2026-01-01" など文字列想定
};
次に、DTOをそのまま使わず、検証・正規化します。よく使われるのがzodなどのスキーマバリデーションです(ライブラリ名を覚える必要はなく、「入力の検品票」だと思ってください)。
import { z } from "zod";
export const customerCreateSchema = z.object({
name: z.string().trim().min(1),
email: z.string().trim().email(),
status: z.enum(["active", "inactive"]).default("active"),
startDate: z.string().trim().optional(), // 後でDateに変換
});
export type CustomerCreateInput = z.infer<typeof customerCreateSchema>;
そしてドメインモデルは「業務として正しい状態」を表します。例えば、メールは正しい形式、ステータスは列挙、開始日はDateとして扱う、などです。ドメイン側で“正しさ”を保持すると、後工程の集計や検索が壊れにくくなります。
export type CustomerStatus = "active" | "inactive";
export type Customer = {
id: string;
name: string;
email: string;
status: CustomerStatus;
startDate: Date | null;
};
export function createCustomer(params: {
id: string;
name: string;
email: string;
status: CustomerStatus;
startDate: Date | null;
}): Customer {
// ここに業務ルール(例:特定ドメインのメール禁止等)があれば集約
return { ...params };
}
APIやServer Actionsでは、DTOを検証してからドメインへ変換します。ここが「境界」であり、変換が一箇所に集まるほど保守が楽になります。
import { customerCreateSchema } from "../forms/customerFormSchema";
import { createCustomer } from "@/domain/customer/Customer";
export async function registerCustomer(dto: unknown) {
const input = customerCreateSchema.parse(dto);
const startDate = input.startDate ? new Date(input.startDate) : null;
if (startDate && Number.isNaN(startDate.getTime())) {
throw new Error("開始日の形式が不正です");
}
const customer = createCustomer({
id: crypto.randomUUID(),
name: input.name,
email: input.email,
status: input.status,
startDate,
});
// repositoryへ保存(DBの型には触れない)
return customer;
}
ここまで分けると、外部都合の変更(フォーム項目追加、API仕様変更)と、業務ルールの変更(ステータス追加、必須条件変更)を切り分けて対応できます。変更が起きる場所を“変換点”に閉じ込めるのが設計のコツです。
失敗しやすいポイントと回避策:現場で崩れない運用ルール
DTOとドメインモデルを分けても、運用で崩れることがあります。よくある失敗と対策を、非エンジニアの意思決定にも役立つ形で整理します。
DTOを「厳格にしすぎて」開発が止まる
理想を追いすぎて、DTO段階で完璧な型を要求すると、外部連携やフォーム入力の例外に対応できず、開発が止まります。対策は、DTOは“受け止める器”と割り切り、厳格さはドメインに入れる直前で担保することです。入力検証は「ユーザーへ分かるエラー」を返すのが目的で、内部の正しさはドメインで守ります。
ドメインモデルがただのinterfaceになってしまう
型だけあって、業務ルールがどこにも書かれていない状態です。これだとDTOとの差がなくなり、分ける意味が薄れます。対策として、ドメインモデル生成関数(例:createCustomer)やドメインサービスに、最低限のルール(許容値、禁止条件、整合性チェック)を置きます。「社内ルールの根拠がコードで追える」状態がE-E-A-Tにも繋がります。
DBの型(ORM)にドメインが引きずられる
PrismaなどのORMは便利ですが、テーブル都合(NULL許容、正規化、リレーション)をそのままドメインへ持ち込むと、業務としての“正”が曖昧になります。対策は、DB行→ドメインへのマッピング(変換)をinfrastructureに閉じ込めることです。DBの変更があっても、変換の一箇所を直せば済む構造を作ります。
Next.jsでクライアント/サーバー境界が曖昧になり漏えいリスクが増える
同じ型をクライアントに共有すると、「本来はサーバーだけが持つべき項目」(例:内部メモ、権限フラグ、原価)が画面に出る事故が起きます。対策は、クライアントへ渡す型はViewModel/Response DTOとして別にし、返してよい項目だけを明示することです。
運用上の“例外”が増えてドメインが汚れる
「過去データだけ形式が違う」「特定取引先だけコード体系が違う」など、現場には例外が必ずあります。対策は、例外をドメインに直書きせず、DTO→正規化の層(もしくはアダプター層)で吸収することです。業務ルールそのものが例外に侵食されると、将来の改善が難しくなります。
3分でできる! 開発費用のカンタン概算見積もりはこちら
まとめ
TypeScriptで型設計を崩さずに運用する鍵は、DTO(外部との受け渡し)とドメインモデル(社内の正しさ)を分けることです。特にNext.jsは便利なぶん境界が見えにくく、API・Server Actions・DB・画面の型が混ざりやすいため、「変換点を明確にする設計」が効きます。
- DTOは現実の揺れ(欠損・表記ゆれ・外部都合)を受け止める
- ドメインモデルは業務ルールを守る“正”のデータだけを扱う
- DTO→検証/正規化→ドメイン変換を一箇所に集め、変更の影響範囲を小さくする
- DBの型や画面表示用の型はドメインと分け、漏えいと改修コストを抑える
もし「いま動いているが、仕様変更のたびに壊れやすい」「外部連携が増えて不安」「情シスとして開発の品質基準を作りたい」といった状況なら、DTOとドメインの分離は、費用対効果が高い改善策になりやすいです。
株式会社ソフィエイトのサービス内容
- システム開発(System Development):スマートフォンアプリ・Webシステム・AIソリューションの受託開発と運用対応
- コンサルティング(Consulting):業務・ITコンサルからプロンプト設計、導入フロー構築を伴走支援
- UI/UX・デザイン:アプリ・Webのユーザー体験設計、UI改善により操作性・業務効率を向上
- 大学発ベンチャーの強み:筑波大学との共同研究実績やAI活用による業務改善プロジェクトに強い
コメント