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 でラップし、total、hasMore、nextCursor などを追加できる余地を残しておくことが推奨されています。
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 つの問題:
- 密結合: DB スキーマの変更が API クライアントに波及する
- セキュリティリスク: パスワードハッシュや内部トークンが漏洩する可能性
- 派生フィールドが作れない:「登録からの経過日数」のような計算値を返せない
- 型の柔軟性がない: DateTime を ISO8601 文字列や Unix タイムスタンプで返したい場合に対応できない
- 関連データの集約ができない: 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 年後の保守性を考えて決めましょう。