API設計の良し悪しは、正常系ではほとんど差が出ません。差が出るのはエラーのときです。「エラーが返ってきたが、何が悪いのか分からない」というAPIは、使う側の実装コストを一気に上げます。
この記事では、抽象論ではなく実際に動いている2つのAPIに壊れたリクエストを投げて、返ってきたものを読みます。題材は GitHub API と、このブログが動いている WordPress の REST API です。以下のレスポンスはすべて実際に取得したものです。
壊れたリクエストを投げてみる
存在しないリソース
$ curl https://api.github.com/repos/no-such-owner/no-such-repo
HTTP 404
{
"message": "Not Found",
"documentation_url": "https://docs.github.com/rest/repos/repos#get-a-repository",
"status": "404"
}
$ curl https://wadokon.com/wp-json/wp/v2/posts/999999
HTTP 404
{
"code": "rest_post_invalid_id",
"message": "無効な投稿 ID です。",
"data": { "status": 404 }
}
ここで既に方針の違いが出ています。WordPress は code という機械可読な識別子を持っていて、GitHub は代わりにドキュメントのURLを返します。
rest_post_invalid_id のような文字列があると、クライアント側で if (err.code === 'rest_post_invalid_id') と分岐できます。メッセージ本文で分岐するのは絶対に避けたいので、この識別子には価値があります。日本語で返ってきていることからも分かるとおり、メッセージは言語設定で変わります。
バリデーションエラー
$ curl https://api.github.com/search/repositories
HTTP 422
{
"message": "Validation Failed",
"errors": [
{ "resource": "Search", "field": "q", "code": "missing" }
],
"documentation_url": "https://docs.github.com/v3/search",
"status": "422"
}
$ curl "https://wadokon.com/wp-json/wp/v2/posts?per_page=999"
HTTP 400
{
"code": "rest_invalid_param",
"message": "無効なパラメータ: per_page",
"data": {
"status": 400,
"params": { "per_page": "per_page は1以上、100以下でなければなりません。" },
"details": {
"per_page": {
"code": "rest_out_of_bounds",
"message": "per_page は1以上、100以下でなければなりません。"
}
}
}
}
どちらも「どのフィールドが、なぜ駄目なのか」を項目ごとに返しています。これがバリデーションエラーの必須要件です。
- GitHub:
errors配列にfieldとcode
複数項目のエラーを同時に返せる - WordPress
detailsにフィールド名をキーとして、項目ごとのcodeとmessage
フォームを実装する側からすると、この構造があるかどうかで実装量が変わります。項目ごとにエラーが返ってくれば、そのまま各入力欄の下に表示できます。「入力に誤りがあります」という文字列が1つ返ってくるだけのAPIだと、どの欄が悪いのかを推測するしかありません。
複数のエラーをまとめて返せるかも重要です。1つ直すたびに次のエラーが出るAPIは、5項目間違えていたら5往復かかります。
400 と 422 は結局どちらなのか

今回まさに、同じ「バリデーション失敗」に対して GitHub は 422、WordPress は 400 を返しました。この使い分けは議論になりがちなところです。
| 状況 | 本来の定義 |
|---|---|
| 400 Bad Request | 構文として解釈できない(壊れたJSONなど) |
| 422 Unprocessable Content | 構文は正しいが、内容が処理できない |
定義どおりなら 422 が正しい場面が多いのですが、実際には400で統一しているAPIも普通にあります。大事なのはどちらを選ぶかより、プロジェクト内で一貫していることです。同じAPIの中で400と422が混ざっているのがいちばん困ります。
401 と 403 も同じ問題がある
認証が必要なエンドポイントに認証なしでアクセスしてみます。
$ curl https://wadokon.com/wp-json/wp/v2/settings
HTTP 401
{
"code": "rest_forbidden",
"message": "その操作を実行する権限がありません。",
"data": { "status": 401 }
}
ここは実例として面白いところです。ステータスコードは 401(未認証)なのに、コードの名前は rest_forbidden(403の意味)になっています。
- 401 Unauthorized
誰か分からない。ログインすれば通るかもしれない - 403 Forbidden
誰かは分かっている。その人には権限がない
この区別は、クライアント側の挙動を変えます。401ならログイン画面へ誘導する、403なら「権限がありません」と表示して終わる。ここが曖昧だと、権限のないユーザーが延々とログイン画面に飛ばされる、といった実装になります。
ちなみに、存在するが権限のないリソースに対して、あえて404を返す設計もあります。403を返すと「そのIDのリソースは存在する」という情報が漏れるためです。これはセキュリティ上の判断で、意図的にやるなら妥当です。

ページネーションはヘッダーで返せる
2つとも、同じ方式を採用していました。
$ curl -D - "https://api.github.com/repos/torvalds/linux/commits?per_page=2"
Link: <https://api.github.com/repositories/2325298/commits?per_page=2&page=2>; rel="next",
<https://api.github.com/repositories/2325298/commits?per_page=2&page=741057>; rel="last"
$ curl -D - "https://wadokon.com/wp-json/wp/v2/posts?per_page=2&page=2"
Link: <https://wadokon.com/wp-json/wp/v2/posts?per_page=2&page=1>; rel="prev",
<https://wadokon.com/wp-json/wp/v2/posts?per_page=2&page=3>; rel="next"
Link ヘッダーに次ページのURLをそのまま入れる方式です。クライアントはページ番号を自分で計算する必要がなく、URLをそのまま叩けばいいのが利点です。
レスポンスボディに {"data": [...], "next": "..."} のように含める方式もあり、こちらのほうが「ヘッダーを見なくていい」ぶん扱いやすい場面もあります。どちらでも構いませんが、ページ番号だけ返して総件数を返さないのは避けたいところです。「次があるかどうか」が分からないと、クライアントは空が返るまで叩き続けることになります。
件数が多いならカーソル方式
ページ番号方式(?page=2)には構造的な弱点があります。
- データが増減すると、ページの境界がずれる
1ページ目を見ている間に新しい項目が追加されると、2ページ目で同じ項目をもう一度見ることになります - 後ろのページほど重くなる
データベース的にはOFFSETなので、10万件目を取るには10万件を読み飛ばす必要があります
「最後に見た項目のID」を渡すカーソル方式(?after=xxx)にすると、どちらも起きません。ただし「5ページ目に飛ぶ」ができなくなります。管理画面のようにページ番号が必要な用途か、無限スクロールのように順に読むだけの用途かで選びます。
レート制限は使う前に分かるように
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1788884674
GitHub はすべての正常なレスポンスにこの3つを付けています。エラーになってから知らせるのではなく、常に残量が見えているのが良い設計です。
クライアント側は、残量を見て自分でペースを落とせます。制限に達したときだけ429を返す設計だと、クライアントは殴ってみるまで分からないので、必ず一度は制限に当たります。
429を返すときは Retry-After を付けます。「いつ再試行していいか」が分からないと、クライアントは適当な間隔でリトライし、結果的に負荷を増やします。
バージョンはパスに置くのが無難
2つとも、パスにバージョンが入っています。
https://wadokon.com/wp-json/wp/v2/posts ← wp/v2
https://docs.github.com/v3/search ← v3
ヘッダーでバージョンを指定する方式もありますが、パスに入っているほうが圧倒的に扱いやすいと思います。ブラウザのアドレスバーに貼れば動きますし、curlでも余計な指定が要りません。ログを見ればどのバージョンが叩かれているかも分かります。
WordPress の wp/v2 という形は、名前空間とバージョンをセットにしている点が参考になります。実際このサイトの API を見ると、wp/v2、oembed/1.0、wp-site-health/v1 のように、機能ごとに別々のバージョンが同居しています。プラグインが独自のAPIを足す前提の設計で、拡張される側のシステムでは有効な考え方です。
リソースの命名
ここは原則が固まっているので短く。
- 名詞の複数形
/posts、/users。動詞は使わず、操作はHTTPメソッドで表す - 階層は浅く
/posts/123/commentsまでにして、それ以上深くしない - ケースを統一
パスは小文字ケバブケース、JSONのキーはスネークかキャメルのどちらかに寄せる
迷うのはリソースに当てはまらない操作です。「パスワードをリセットする」「メールを再送する」のような処理は名詞にしづらいところですが、POST /password-reset-requests のように「その操作の記録」をリソースと考えると収まることが多いです。
それでも無理なら POST /users/123/resend-invitation のような書き方で構いません。原則にこだわって分かりにくくなるくらいなら、例外として明示するほうが良いと思います。
まとめ
- エラーには機械可読な
codeを入れる
メッセージ本文で分岐させない - バリデーションエラーは項目ごとに、まとめて返す
1往復で全部分かるようにする - 400と422、401と403の使い分けは、一貫していることが最優先
混在がいちばん困る - 次ページのURLを返す
件数が多いならカーソル方式を検討する - レート制限は常に残量を返す
429だけでは遅い。Retry-Afterも付ける - バージョンはパスに置く
curlで叩けることの価値は大きい
設計に迷ったときは、実際に使われているAPIに壊れたリクエストを投げてみるのが手っ取り早いと思います。今回のように、こちらから2〜3回叩くだけで、そのAPIがエラーをどう扱う方針なのかがはっきり見えます。
次に読む記事



