API設計アンチパターン7選 - UI都合の設計が招く技術的負債

開発

WEB サービスを作っていると、必然的に API も設計することになります。でも、僕が見てきた限り、UI都合の設計をしている人がとても多いんですよね。

たとえば、一覧画面で ID を表示しないからといって、レスポンスから ID を省略する。これ、よく見るパターンですけど、正直いけてないです。

今回は、こういった API 設計のアンチパターンを 7 つピックアップして、なぜダメなのか、どう直すべきなのかを具体的に解説します。海外の最新記事から得た知見も織り交ぜながら、実践的な内容にまとめました。

1. UIに表示しないからといってIDを返さない

これが一番よく見るアンチパターンです。

「一覧画面に ID は表示しないから、レスポンスに含めなくていいよね」という発想。気持ちはわかります。でも、これはInside-out設計と呼ばれる、典型的なアンチパターンなんです。

海外の記事では、この問題を「内部システムから設計を始める」アプローチとして批判しています。Stripe のような優れた API は、逆に「Outside-in」、つまり API の消費者が何を必要とするかから設計を始めます。

// ❌ アンチパターン: UIに表示しないからIDを省略
{
  "users": [
    { "name": "田中太郎", "email": "tanaka@example.com" },
    { "name": "山田花子", "email": "yamada@example.com" }
  ]
}

// ✅ 正しい設計: 常にIDを含める
{
  "users": [
    { "id": "user_123", "name": "田中太郎", "email": "tanaka@example.com" },
    { "id": "user_456", "name": "山田花子", "email": "yamada@example.com" }
  ]
}

ID があれば、クライアントは詳細画面への遷移ができます。削除・更新のリクエストも送れますし、ローカルのキャッシュと照合できます。React 等での key 属性にも使えます。

GraphQL なら尚更です。クライアント側で必要なフィールドを選択できるのが GraphQL の強みなのに、サーバー側でフィールドを削ってしまったら意味がありません。

ちなみに、「一覧と詳細でエンドポイントを分けて、一覧に含める項目を UI に合わせて絞るのも UI 都合では?」と思うかもしれません。僕もそう思って AI に聞いてみたんですが、返ってきた答えは「それはユースケース都合」でした。

一覧で必要な情報と詳細で必要な情報が違うのは当然で、それに合わせてレスポンスを最適化するのは正しい設計です。問題なのは、「UI に表示しないから」という理由だけで ID のような汎用的に必要なフィールドを削ってしまうこと。ユースケースを考えれば、ID は一覧でも必要なはずです。

DDD の観点からも同じことが言えます。ユーザーや商品といったデータは Entity であり、Entity は「同一性(Identity)」によって識別されます。

名前やメールアドレスが同じでも、別のユーザーは別の Entity。つまり、Entity を返す以上、ID は概念的に不可分なんです。「UI に表示するかどうか」ではなく、そのデータの本質として ID が必要という話ですね。

2. 配列を直接返してしまう

これもよく見ます。

// ❌ アンチパターン: 配列を直接返す
[
  { "id": 1, "name": "商品A" },
  { "id": 2, "name": "商品B" }
]

// ✅ 正しい設計: オブジェクトでラップする
{
  "data": [
    { "id": 1, "name": "商品A" },
    { "id": 2, "name": "商品B" }
  ],
  "total": 100,
  "hasMore": true
}

配列を直接返すと、後からページネーション情報やメタデータを追加しようとしたとき、破壊的変更が必要になります。既存のクライアントが全部壊れます。

海外のベストプラクティスでは、最初から data でラップし、totalhasMorenextCursor などを追加できる余地を残しておくことが推奨されています。

3. DBのモデルをそのままAPIレスポンスにする

開発を急いでいると、ついやってしまいがちです。

// ❌ アンチパターン: Prismaのモデルをそのまま返す
const user = await prisma.user.findUnique({ where: { id } })
return Response.json(user)

// ✅ 正しい設計: 変換レイヤーを挟む
const user = await prisma.user.findUnique({ where: { id } })
return Response.json({
  id: user.id,
  name: user.name,
  email: user.email,
  createdAt: user.createdAt.toISOString(),
  // passwordHashは返さない
  // internalFlagsは返さない
})

海外の記事で指摘されている、DB モデル直接公開の 5 つの問題:

  1. 密結合: DB スキーマの変更が API クライアントに波及する
  2. セキュリティリスク: パスワードハッシュや内部トークンが漏洩する可能性
  3. 派生フィールドが作れない:「登録からの経過日数」のような計算値を返せない
  4. 型の柔軟性がない: DateTime を ISO8601 文字列や Unix タイムスタンプで返したい場合に対応できない
  5. 関連データの集約ができない: GitHub のようにリポジトリ情報と一緒にオーナー情報も返す、といったことが難しい

4. エラーレスポンスが雑

「とりあえず動く」を優先すると、エラーハンドリングが雑になりがちです。

// ❌ アンチパターン: 何が起きたかわからない
{ "error": "エラーが発生しました" }

// ❌ もう一つのアンチパターン: 常に200を返す
HTTP 200 OK
{ "success": false, "error": "ユーザーが見つかりません" }

海外では**RFC 9457(Problem Details for HTTP APIs)**に基づいた構造化エラーが推奨されています。

// ✅ RFC 9457に基づく構造化エラー
HTTP 400 Bad Request
{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 400,
  "detail": "リクエストに不正なフィールドが含まれています",
  "errors": [
    {
      "field": "email",
      "reason": "有効なメールアドレス形式ではありません",
      "value": "invalid-email"
    }
  ]
}

このフォーマットなら、typeでエラーの種類を機械的に判別できます。titleで人間が読める要約がわかり、statusで HTTP ステータスコードが明示されます。errors配列を使えば、複数のバリデーションエラーを一度に返せます。

5. Nullable Fieldsを乱用する

これは意外と見落とされがちですが、海外の記事では明確にアンチパターンとされています。

// ❌ アンチパターン: nullableなBoolean
{
  "isActive": null  // true? false? 不明?
}

// ❌ アンチパターン: nullableなEnum
{
  "status": null  // どの状態?
}

// ✅ 正しい設計: フィールドを省略する
{
  // isActiveが不要なら、フィールド自体を含めない
}

// ✅ または明示的な状態を追加
{
  "status": "unknown"  // nullではなく明示的な値
}

null には「値がない」「まだ設定されていない」「意図的に空」など複数の意味がありえます。この曖昧さがバグの温床になります。

**Postel の法則(頑健性の原則)**を適用するなら、「送信するものは厳密に、受け入れるものは寛容に」です。null を返すのではなく、フィールドを省略する設計の方が明確です。

6. バージョニングをURLパスで管理しすぎる

/api/v1/users
/api/v2/users
/api/v3/users

これ、よく見ますよね。でも海外の最新のベストプラクティスでは、可能な限りバージョニングを避けることが推奨されています。

// ❌ アンチパターン: バージョンを分けすぎ
GET /api/v1/users  // 古いフォーマット
GET /api/v2/users  // 新しいフォーマット
GET /api/v3/users  // さらに新しいフォーマット

// ✅ 推奨: フィールドを追加してから置き換える
// Phase 1: 新フィールドを追加(後方互換を維持)
{
  "name": "田中太郎",           // 古いフィールド
  "fullName": "田中 太郎",      // 新しいフィールド
  "firstName": "太郎",
  "lastName": "田中"
}

// Phase 2: ドキュメントで古いフィールドの廃止予定を告知
// Phase 3: 十分な移行期間後に古いフィールドを削除

バージョンを 3 つ並行運用すると、バグ修正を 3 回行う必要があります。保守コストが 3 倍です。

代わりに、クエリパラメータで選択機能を提供する方法もあります。

GET /users/{id}?include=habits,entries

7. POST一本槍でHTTPメソッドを使い分けない

// ❌ アンチパターン: すべてPOST
POST /api/getUsers
POST /api/createUser
POST /api/deleteUser

// ✅ 正しい設計: HTTPメソッドを適切に使う
GET    /api/users      // 一覧取得
POST   /api/users      // 新規作成
GET    /api/users/:id  // 詳細取得
PUT    /api/users/:id  // 全体更新
PATCH  /api/users/:id  // 部分更新
DELETE /api/users/:id  // 削除

POST ですべてを処理しようとすると、まずキャッシュが効きません。GET ならブラウザ・CDN・プロキシでキャッシュ可能です。

冪等性も保証されなくなります。PUT や DELETE は冪等(何度実行しても同じ結果)であるべきです。さらに、予測可能性が失われ、開発者がドキュメントなしで API を使えなくなります。

まとめ

API 設計のアンチパターンは、ほとんどが「目の前の UI や実装の都合を優先した結果」です。

正しいアプローチは次のとおりです。まず Outside-in 設計で、API の消費者が何を必要とするかから考えます。常に ID を含め、UI に表示しなくてもクライアントには必要です。

配列はオブジェクトでラップして将来のメタ情報追加に備えます。変換レイヤーを挟み、DB モデルと API レスポンスは分離します。RFC 9457 でエラーを構造化し、曖昧なエラーメッセージは避けます。

null より省略して曖昧さをなくします。HTTP メソッドを正しく使い、キャッシュと冪等性を活用します。

特に、GraphQL を使っているなら、フィールドの省略は本末転倒です。クライアントが選択できるのが GraphQL の強みなのに、サーバー側で制限してしまっては意味がありません。

API 設計は、今日の便利さより、半年後・1 年後の保守性を考えて決めましょう。

Thanks for reading!
API設計REST APIアンチパターンTypeScriptバックエンド