API型設計をTypeScriptで考える実務ポイント

TypeScript API型設計でレスポンス型DTO画面用モデルを分ける図

外部境界から考える

API型設計をTypeScriptで考えるとき、レスポンス型をそのまま画面で使っていないでしょうか。

小さな画面なら、それでも動きます。しかし、実務ではAPI仕様の変更、nullableな値、表示用の加工が増えます。その結果、画面側に条件分岐が散らばりやすくなります。

この記事では、API型設計でレスポンス型、DTO、画面用モデルを分ける理由を整理します。目的は、API変更に強く、レビューしやすい型設計にすることです。

API型設計でレスポンス型をそのまま使わない理由

まず、APIレスポンス型は外部から来るデータです。つまり、フロントエンド側で完全には制御できません。

例えば、APIから次のようなユーザー情報が返るとします。

type UserResponse = {
  id: string;
  first_name: string;
  last_name: string;
  status: "active" | "inactive";
  last_login_at: string | null;
};

この型をそのまま画面に渡すと、画面側がAPIの命名に依存します。また、日付表示やステータス表示の加工も画面に散らばります。

そのため、実務ではAPIレスポンス型と画面用モデルを分ける方が保守しやすくなります。

APIレスポンス型と画面用モデルを分ける

次に、APIレスポンス型を画面用モデルへ変換します。ここで命名、nullable、表示用の値を整理します。

type UserViewModel = {
  id: string;
  fullName: string;
  statusLabel: string;
  lastLoginText: string;
};

function toUserViewModel(user: UserResponse): UserViewModel {
  return {
    id: user.id,
    fullName: `${user.last_name} ${user.first_name}`,
    statusLabel: user.status === "active" ? "有効" : "停止中",
    lastLoginText: user.last_login_at ?? "未ログイン",
  };
}

この変換を入れると、画面コンポーネントは表示に集中できます。また、API側の命名やnullの扱いを変換層に閉じ込められます。

一方で、すべての小さなAPIで過剰にモデルを分ける必要はありません。複数画面で使うデータや、加工が多いデータから分けると現実的です。

API型設計でDTOをどう扱うか

DTOという言葉は、現場によって意味が揺れやすいです。フロントエンドでは、APIとやり取りするためのデータ型として使うことが多いです。

実務では、次のように分けると整理しやすくなります。

役割
Response型APIから返る値UserResponse
Request型APIへ送る値UpdateUserRequest
ViewModel画面で使う値UserViewModel
FormValuesフォーム入力値UserFormValues

このように名前を分けると、レビュー時に責務を確認しやすくなります。また、APIへ送る値と画面で持つ値を混同しにくくなります。

unknownと実行時検証でAPI境界を守る

TypeScriptの型は、実行時のデータを保証しません。fetchで受け取ったJSONは、型注釈を書いても本当にその形とは限りません。

そのため、外部から来る値はunknownとして受ける考え方が役に立ちます。必要に応じて、型ガードやスキーマ検証で確認します。

async function fetchJson(url: string): Promise<unknown> {
  const response = await fetch(url);
  return response.json();
}

型の絞り込みについては、TypeScript公式のNarrowingも参考になります。

また、APIレスポンスを実行時に検証したい場合は、Zodのようなスキーマ検証ライブラリを使う選択肢もあります。

API型設計でレビューされやすいポイント

API型設計では、型があるかどうかだけでなく、型の置き場所がレビューされます。特に次の観点は見られやすいです。

  • APIレスポンス型を画面に直接渡していないか
  • Request型とResponse型を混同していないか
  • nullやundefinedの扱いが画面に散らばっていないか
  • 表示用の文字列変換がコンポーネントに入りすぎていないか
  • 外部データをanyで受け取っていないか

なお、Utility Typesを使う場合は、意図が読みやすいかも確認しましょう。公式のUtility Typesも参考になります。

よくある質問

TypeScriptでAPIレスポンス型は必ず分けるべきですか?

必ずではありません。ただし、複数画面で使うデータや加工が多いデータは分けた方が保守しやすいです。

DTOとViewModelの違いは何ですか?

DTOはAPIとの入出力に近い型です。ViewModelは画面表示に近い型です。責務を分けると、API変更の影響を抑えやすくなります。

APIレスポンスはanyで受けてもよいですか?

一時的には使えます。ただし、anyのまま画面へ広げると型安全が崩れます。unknownや実行時検証を使う方が安全です。

zodは必ず必要ですか?

必須ではありません。ただし、外部APIや仕様変更が多いAPIでは、実行時検証を入れる価値があります。

まとめ:API型設計は境界を分けると保守しやすい

API型設計では、APIレスポンス型をそのまま画面で使わないことが重要です。

  • APIレスポンス型は外部境界として扱う
  • 画面用モデルへ変換する層を作る
  • Request型とResponse型を分ける
  • unknownや実行時検証で外部データを守る
  • コンポーネントは表示に集中させる

API型設計を整理すると、Reactコンポーネントも読みやすくなります。また、仕様変更時の影響範囲も追いやすくなります。

ReactやTypeScriptの経験を活かせる案件について整理したい方は、カジュアル面談で相談することもできます。また、案件内容や支援体制を知りたい方は、bluenaの採用情報も参考にしてください。

「TypeScriptの経験を次の案件でどう活かすか」に迷っているなら、まずbluenaに話してみてください。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア