言った言わないを防ぐ。 フロントと 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 の議事録。
書いておけば、食い違いはコンパイルが教えてくれる。
あなたのプロジェクトでも、型契約を試してみませんか。

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