見出し画像

言った言わないを防ぐ。 フロントと API の型契約

〜 Client / Server 両側から考える型設計 〜

フロントと API の間で、言った言わないが起きたことはありませんか。

「このフィールド、返すって言ったよね」
「いや、そんな話してない」

——人間同士なら議事録で解決できる。
フロントと API のあいだでは、型が契約になるのです。


フロントと API の言った言わないとは

僕が個人開発でフロントと API を両方触っているとき、何度か言った言わないに近いことが起きました。

📍 API のレスポンスに createdAt を追加した。
 フロントは created_at を期待していた。

📍 フロントでこの型で受け取ると決めていたのに、API 側でフィールド名を変えてしまった。

📍 仕様は口頭やチャットで共有していたが、型として残っていなかった。

そのたびに、フロントでランタイムエラーや表示崩れが起き、原因を追う時間が奪われました。

言った言わないを防ぐには、型を契約として明文化するしかない。

そう気づいてから、Client / Server 両側から型を揃える設計に切り替えました。

型を契約として明文化するとは

型を契約として明文化する——
それは、フロントと API のあいだで何を渡し、何を受け取るかを、型定義として共有するということです。

📌 リクエストの型
📌 レスポンスの型
📌 エラーの型

これらを、どちらか一方だけが持つのではなく、両側で参照できる形で置く。

そうすると、API が返す形とフロントが期待する形が食い違った瞬間に、型エラーで検知できる。

言った言わないが、コンパイル時に消えるのです。

Client / Server 両側から考える型設計

型を契約として明文化するには、Client と Server の両側から型を設計する必要があります。

片側だけの型定義では足りない

フロントだけが型を定義していると、API が変わったときに気づけない。
API だけが型を定義していると、フロントが何を期待しているかが伝わらない。

両側が同じ型定義を参照するか、一つのスキーマから両側の型を生成するか。

どちらかの設計にしないと、契約は成立しません。

契約の置き場所

共有パッケージ:
型定義を packages/types のような共有パッケージに置き、フロントも API もそこを参照する。

API スキーマから生成:
OpenAPI や Zod スキーマを正とし、そこから TypeScript の型を生成する。
フロントはその型を import する。

単一リポジトリ内の共有ファイル:
モノレポなら、shared/types に型を置き、両側から import する。

どの方式を選ぶかは、プロジェクトの規模や構成によります。
大切なのは、型定義が一箇所にあり、両側がそれを参照していることです。

型契約を破ったとき、 コンパイルが教えてくれる

型を契約として明文化しておくと、契約を破った瞬間にコンパイルエラーが出ます。

📌 API のレスポンス型からフィールドを削除した
👉️ フロントでそのフィールドを参照している箇所が型エラーになる。

📌 フロントが新しいフィールドを期待するように型を更新した
👉️ API がまだ返していなければ、フロントの型と API の実装の不一致が、型の更新作業で明らかになる。

言った言わないは、ランタイムで発覚するのではなく、ビルド時に検知できるようになります。

【型契約の具体的な設計】 3 つのパターン

僕が実践している、型契約の設計パターンを 3 つ紹介します。

1️⃣ 共有型ファイルを両側から import する (最小構成)

モノレポや同一リポジトリ内で、shared/types/api.ts のようなファイルに型を定義する。

// shared/types/api.ts
export type UserResponse = {
  id: string;
  name: string;
  email: string;
  createdAt: string; // ISO 8601
};

フロントと API の両方が、このファイルを import して使う。
API はこの型に沿ってレスポンスを返し、フロントはこの型で受け取る。

メリット:
シンプルで、小規模なプロジェクトにすぐ導入できる。

デメリット:
ランタイムバリデーションは別途必要(Zod など)。

2️⃣ Zod スキーマを正にして型を推論する

Zod でスキーマを定義し、z.infer で型を導出する。

// shared/schemas/user.ts
import { z } from 'zod';

export const UserResponseSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  createdAt: z.string(),
});

export type UserResponse = z.infer<typeof UserResponseSchema>;

API 側では、レスポンスを返す前に UserResponseSchema.parse(data) でバリデーションする。
フロント側では、UserResponse 型で受け取る。

メリット:
ランタイムバリデーションと型が一つのスキーマから得られる。
不正なデータがフロントに届くのを防げる。

デメリット:
Zod の導入が必要。

3️⃣ OpenAPI から型を生成する

OpenAPI 仕様を書いておき、そこから TypeScript の型を自動生成する。
openapi-typescript などのツールを使う。

メリット:
API の仕様がドキュメント化され、フロント・API・他サービスが同じ仕様を参照できる。

デメリット:
OpenAPI の記述とメンテナンスのコストがかかる。
小規模だと過剰になることがある。

型契約を守るためのルール

型契約を設計したあと、守り続けるためのルールを決めておくとよいです。

📌 型の変更は、両側の影響を確認してから行う
片側だけ変えると、もう片側で型エラーが出る。
意図的に壊すなら、両側を同時に直す。

📌 とりあえず any を禁止する
型が曖昧になると、契約が崩れる。
any で逃げず、正しい型を定義する。

📌 リクエスト・レスポンスの型は、必ず共有定義から参照する
フロントと API で別々に型を書かない。
一箇所で定義し、両側が参照する。

この 3 つを守るだけでも、言った言わないはかなり減ります。

型は、 フロントと API の議事録

人間同士の言った言わないには、議事録がある。

フロントと API の言った言わないには、がある。

型を契約として明文化し、Client / Server 両側から揃える。

それだけで、仕様の食い違いで泣く時間が減ります。

あわせて読みたい

▼ 境界や責務を先に決める設計を知りたいときに

フロントとサーバーの境界を型やバリデーションで守る設計。
型契約の考え方と通じます。

▼ スキーマや型を後回しにしない設計を知りたいときに

型やスキーマを最初に決めておく重要性。
API の型契約も同じ思想です。

▼ フォームやバリデーションを型で守りたいときに

Zod でスキーマを定義し、型とランタイムバリデーションを揃える実例です。

▼ 設定や環境変数を型で守りたいときに

Zod でスキーマを定義し、型とバリデーションを一体化する考え方の参考に。

▼ 認可や境界を最初に決める設計を知りたいときに

最初に決める設計思想。
型契約も同じく、最初に決めておくことで手戻りを防ぎます。

▼ API やバックエンドの層設計を知りたいときに

API の責務や層を整理する設計。
型契約は、その境界を守る役割も果たします。

ひとりごと

フロントと API の言った言わないで、何度か深夜デバッグをした経験があります。

型を契約として明文化してから、その手戻りは激減しました。

型は、フロントと API の議事録。
書いておけば、食い違いはコンパイルが教えてくれる。

あなたのプロジェクトでも、型契約を試してみませんか。

2026© おおとろ

ここから先は

0字

メンバーシップ ¥ 500 /月

■メンバーシップ 15 年目フリーランス開発者のひとり戦略を支える作戦会議室です。\ 案件に追われる…

ベーシックプラン

¥500 / 月

最後まで読んでいただき、ありがとうございます。もしこの記事が「役に立った」「心に響いた」と感じたら、珈琲一杯分サポートいただけると嬉しいです。あなたの温かい応援を力に、また次の創作活動に励みます。