REST APIを設計・実装するときに押さえておきたい原則と慣習をまとめたガイドです。
目次
- リソース設計とURI
- HTTPメソッド
- HTTPステータスコード
- リクエスト / レスポンス形式
- エラーハンドリング
- ページネーション・フィルタ・ソート
- バージョニング
- 認証・認可とセキュリティ
- 冪等性と並行制御
- キャッシュ
- レート制限
- 非同期処理
- ドキュメントと運用
- チェックリスト
1. リソース設計とURI
基本ルール
| ルール | 良い例 | 悪い例 |
|---|---|---|
| 名詞を使う(動詞はHTTPメソッドで表す) | GET /users | GET /getUsers |
| コレクションは複数形 | /users, /orders | /user, /order |
| 小文字 + ケバブケース | /order-items | /OrderItems, /order_items |
| 末尾スラッシュなし | /users/123 | /users/123/ |
| ファイル拡張子を含めない | /users/123(Acceptで指定) | /users/123.json |
階層(サブリソース)
親子関係が明確な場合のみネストし、深さは 2階層程度まで に抑えます。
GET /users/123/orders # ユーザー123の注文一覧
GET /users/123/orders/456 # 深すぎる場合は ↓ を検討
GET /orders/456 # 注文IDが一意なら直接アクセス
CRUDに収まらない操作
どうしても動詞的な操作が必要な場合は、以下のいずれかで表現します。
# 1. サブリソースとして表現(推奨)
POST /orders/456/cancellation
# 2. アクションをサブパスにする
POST /orders/456/cancel
# 3. Google AIP流のカスタムメソッド
POST /orders/456:cancel
チーム内でどれか1つに統一することが重要です。
IDの扱い
- 連番IDは推測・列挙されやすいため、外部公開するならUUIDやULIDなどの推測困難なIDを検討する
- IDは文字列として扱う(クライアント側の数値精度問題を避けるため)
2. HTTPメソッド
| メソッド | 用途 | 安全 | 冪等 | ボディ |
|---|---|---|---|---|
GET | 取得 | ✅ | ✅ | なし |
POST | 作成・処理の実行 | ❌ | ❌ | あり |
PUT | 全体置換(存在しなければ作成も可) | ❌ | ✅ | あり |
PATCH | 部分更新 | ❌ | ❌※ | あり |
DELETE | 削除 | ❌ | ✅ | 通常なし |
HEAD | ヘッダのみ取得 | ✅ | ✅ | なし |
OPTIONS | 対応メソッドの確認(CORSプリフライト) | ✅ | ✅ | なし |
※ PATCHは実装次第で冪等にできます。
注意点
- GETで状態を変更しない(クローラーやプリフェッチで意図せず実行される)
PUTとPATCHを使い分ける。PUTで送られなかったフィールドは「削除/初期化」と解釈されるのが本来の意味- PATCHの形式は JSON Merge Patch (RFC 7396) または JSON Patch (RFC 6902) を採用すると仕様が明確になる
3. HTTPステータスコード
よく使うコード
2xx 成功
| コード | 用途 |
|---|---|
200 OK | 一般的な成功(GET, PUT, PATCH) |
201 Created | リソース作成成功。Location ヘッダで新リソースのURIを返す |
202 Accepted | 非同期処理を受け付けた |
204 No Content | 成功・返すボディなし(DELETEなど) |
3xx リダイレクト
| コード | 用途 |
|---|---|
301 / 308 | 恒久的な移動(308はメソッドを維持) |
304 Not Modified | 条件付きGETでキャッシュが有効 |
4xx クライアントエラー
| コード | 用途 |
|---|---|
400 Bad Request | リクエストの形式不正(JSONが壊れている等) |
401 Unauthorized | 未認証(認証情報がない・無効) |
403 Forbidden | 認証済みだが権限がない |
404 Not Found | リソースが存在しない |
405 Method Not Allowed | メソッド非対応(Allow ヘッダを返す) |
409 Conflict | 状態の競合(重複作成、楽観ロック失敗など) |
412 Precondition Failed | If-Match 等の前提条件不一致 |
415 Unsupported Media Type | 非対応の Content-Type |
422 Unprocessable Content | 形式は正しいがバリデーションエラー |
429 Too Many Requests | レート制限超過(Retry-After を返す) |
5xx サーバーエラー
| コード | 用途 |
|---|---|
500 Internal Server Error | 予期しないサーバーエラー |
502 Bad Gateway | 上流サーバーからの不正応答 |
503 Service Unavailable | 一時的に利用不可(Retry-After を返す) |
504 Gateway Timeout | 上流サーバーのタイムアウト |
アンチパターン
- エラーでも
200 OKを返し、ボディに"success": falseを入れる - すべてのエラーを
400や500にまとめる - 権限のないリソースの存在を隠したい場合は
403ではなく404を返すことも検討する
4. リクエスト / レスポンス形式
基本
Content-Type: application/jsonを標準にする(文字コードはUTF-8)- フィールド名の命名規則を 統一 する(
camelCaseかsnake_caseのどちらか) - 日時は ISO 8601 / RFC 3339 形式で、タイムゾーンを含める
{
"id": "01HF8X2K3M4N5P6Q7R8S9T0V1W",
"name": "山田 太郎",
"email": "taro@example.com",
"createdAt": "2026-04-01T09:30:00Z",
"updatedAt": "2026-04-02T12:00:00+09:00"
}設計のポイント
- 金額は小数(float)を避ける:最小通貨単位の整数か、文字列の10進数で表す
{ "amount": 1500, "currency": "JPY" } - null と欠落の扱いを決める:「値がない」を
nullで返すのか、キーごと省略するのかを統一する - 列挙値は文字列 にする(
"status": "shipped")。数値コードは可読性が低い - 真偽値は肯定形 で命名する(
isActive○ /isNotDeleted×) - トップレベルは配列ではなくオブジェクト にする(メタ情報を後から追加できる)
{ "data": [ ... ], "pagination": { ... } } - レスポンスに内部実装を漏らさない(DBのカラム名、スタックトレース、内部IDなど)
部分取得・関連リソースの展開
必要に応じて、フィールドの絞り込みや関連の展開を提供します。
GET /users/123?fields=id,name,email
GET /orders/456?expand=customer,items
5. エラーハンドリング
統一フォーマットを使う
RFC 9457 (Problem Details for HTTP APIs) に準拠すると、標準化されたエラー形式になります。
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json{
"type": "https://api.example.com/problems/validation-error",
"title": "入力内容に誤りがあります",
"status": 422,
"detail": "2件のフィールドでバリデーションエラーが発生しました",
"instance": "/users",
"traceId": "a1b2c3d4e5f6",
"errors": [
{ "field": "email", "code": "invalid_format", "message": "メールアドレスの形式が正しくありません" },
{ "field": "age", "code": "out_of_range", "message": "0以上150以下で指定してください" }
]
}ポイント
- 機械可読なエラーコード(
code/type)と 人間向けメッセージ を分ける。クライアントはメッセージ文字列で分岐しない - バリデーションエラーは 全フィールドをまとめて 返す(1件ずつだと往復が増える)
- トレースID / リクエストID を含め、ログと突き合わせられるようにする
- 5xxでは内部情報(SQL、スタックトレース、ファイルパス)を返さない
6. ページネーション・フィルタ・ソート
ページネーション
| 方式 | 例 | 長所 | 短所 |
|---|---|---|---|
| オフセット | ?offset=40&limit=20 | 実装が簡単、任意ページへ移動可 | 大きいオフセットで遅い、データ追加時にずれる |
| ページ番号 | ?page=3&perPage=20 | UIと相性が良い | オフセットと同じ欠点 |
| カーソル(推奨) | ?cursor=eyJpZCI6MTAwfQ&limit=20 | 高速・一貫性が高い | 任意ページへジャンプできない |
カーソル方式のレスポンス例:
{
"data": [ ... ],
"pagination": {
"nextCursor": "eyJpZCI6MTIwfQ",
"hasMore": true
}
}limitには デフォルト値と上限 を設ける(例:デフォルト20、最大100)- 総件数(
totalCount)は大規模データでは高コストなので、必要な場合のみ返す
フィルタ
GET /orders?status=shipped&customerId=123
GET /orders?createdAt[gte]=2026-01-01&createdAt[lt]=2026-02-01
GET /products?tags=sale,new
ソート
GET /products?sort=-price,name # price降順 → name昇順
検索
全文検索など複雑な条件は専用エンドポイントにするのも一案です。
GET /products/search?q=キーボード
POST /products/search # 条件が複雑でURLに収まらない場合
7. バージョニング
方式
| 方式 | 例 | 備考 |
|---|---|---|
| URIパス(最も一般的) | /v1/users | わかりやすく、ルーティング・キャッシュが容易 |
| ヘッダ | Api-Version: 2026-04-01 | URIがきれい。日付ベース(Stripe方式)も有名 |
| メディアタイプ | Accept: application/vnd.example.v2+json | RESTの考え方に忠実だが扱いが煩雑 |
破壊的変更と非破壊的変更
非破壊的(バージョンを上げなくてよい)
- 新しいエンドポイントの追加
- レスポンスへの任意フィールド追加
- 任意のリクエストパラメータ追加
破壊的(新バージョンが必要)
- フィールドの削除・名前変更・型変更
- 必須パラメータの追加
- ステータスコードやエラー形式の変更
- デフォルト挙動の変更
運用
- クライアントは 未知のフィールドを無視する よう実装してもらう(前方互換性)
- 廃止予定のAPIは
Deprecation/Sunsetヘッダで事前に通知するDeprecation: @1767225600 Sunset: Thu, 31 Dec 2026 23:59:59 GMT Link: <https://api.example.com/docs/migration-v2>; rel="deprecation"
8. 認証・認可とセキュリティ
認証
- 常にHTTPS(TLS) を使う。HTTPは受け付けないかリダイレクトではなく拒否する
- ユーザー委任のアクセスには OAuth 2.0 / OpenID Connect を使う
- トークンは
Authorization: Bearer <token>ヘッダで送る。URLのクエリに含めない(ログやRefererに残る) - アクセストークンは短命にし、リフレッシュトークンで更新する
- サーバー間通信のAPIキーはローテーション可能にしておく
認可
- リソース単位で権限チェック を行う(IDを書き換えて他人のデータにアクセスできるBOLA/IDOR脆弱性を防ぐ)
- 一括代入(Mass Assignment)を防ぐ:
roleやisAdminのようなフィールドをリクエストから直接更新させない - 最小権限の原則に従い、スコープを細かく設計する
入力と出力
- すべての入力をサーバー側でバリデーションする(型、長さ、範囲、形式)
- リクエストボディサイズに上限を設ける
- SQLインジェクション等を防ぐため、パラメータ化クエリを使う
- レスポンスには必要なデータだけを返す(過剰なデータ露出を避ける)
ヘッダ
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Cache-Control: no-store # 機密データを含むレスポンス- CORSは
Access-Control-Allow-Origin: *を安易に使わず、許可するオリジンを明示する
参考
9. 冪等性と並行制御
冪等性キー
ネットワーク障害でクライアントがリトライしても二重処理されないよう、POST に冪等性キーを導入します(決済・注文などで特に重要)。
POST /payments
Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324- サーバーはキーと結果を一定期間保存し、同じキーの再送には 同じレスポンス を返す
- 同じキーで異なるリクエスト内容が来たら
422や409でエラーにする
楽観的ロック(ETag)
同時更新による上書き(Lost Update)を防ぎます。
GET /documents/42
→ ETag: "v7"
PUT /documents/42
If-Match: "v7"
→ 他者が先に更新していれば 412 Precondition Failed10. キャッシュ
Cache-Controlで明示的にキャッシュ方針を指定するCache-Control: public, max-age=3600 # 公開データ Cache-Control: private, max-age=60 # ユーザー固有データ Cache-Control: no-store # 機密データETag/Last-Modifiedで条件付きリクエストに対応し、変更がなければ304 Not Modifiedを返すGET /products/1 If-None-Match: "abc123" → 304 Not Modified- レスポンスが
Accept-Language等で変わる場合はVaryヘッダを付ける
11. レート制限
- 制限を超えたら
429 Too Many RequestsとRetry-Afterを返す - 残り回数をヘッダで通知する
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30- 制限単位(APIキー / ユーザー / IP)とウィンドウ(秒 / 分 / 日)をドキュメントに明記する
- クライアント側には 指数バックオフ + ジッター でのリトライを推奨する
12. 非同期処理
時間のかかる処理(レポート生成、一括インポートなど)は同期で待たせず、ジョブとして扱います。
POST /reports
→ 202 Accepted
Location: /jobs/789
GET /jobs/789
→ 200 OK
{ "id": "789", "status": "running", "progress": 45 }
GET /jobs/789
→ 200 OK
{ "id": "789", "status": "succeeded", "resultUrl": "/reports/321" }- 完了通知が必要な場合は Webhook を提供する
- 署名(HMAC等)を付けて送信元を検証できるようにする
- 受信側の失敗に備えてリトライし、イベントIDで重複排除できるようにする
13. ドキュメントと運用
ドキュメント
- OpenAPI で仕様を記述し、ドキュメント・クライアントSDK・モックサーバーを生成する
- 各エンドポイントにリクエスト / レスポンス例、エラー例、必要な権限を記載する
- 変更履歴(Changelog)を公開する
監視・運用
- すべてのリクエストに リクエストID を付与し、レスポンスヘッダでも返す(例:
X-Request-Id) - 分散トレーシング(W3C Trace Context / OpenTelemetry)を導入する
- レイテンシ、エラー率、スループットを監視し、SLOを定める
- ヘルスチェック用エンドポイントを用意する(例:
GET /health) - ログに個人情報やトークンを出力しない
テスト
- 契約テスト(Contract Testing)でOpenAPI仕様と実装の乖離を検出する
- 境界値・異常系(不正な入力、権限なし、存在しないID)のテストを書く
14. チェックリスト
設計レビュー時の確認用です。
URI・メソッド
- URIは名詞・複数形・小文字ケバブケースになっている
- ネストは2階層程度に収まっている
- HTTPメソッドを意味どおりに使っている(GETで状態を変更していない)
レスポンス
- 適切なステータスコードを返している
- フィールドの命名規則が統一されている
- 日時はISO 8601(タイムゾーン付き)
- 金額に浮動小数点を使っていない
- エラーは統一フォーマット(RFC 9457など)で返している
一覧取得
- ページネーションがあり、
limitに上限がある - フィルタ・ソートの仕様が一貫している
セキュリティ
- HTTPSのみ
- リソースごとに認可チェックをしている
- トークンをURLに含めていない
- 入力バリデーションとボディサイズ制限がある
- レート制限がある
信頼性
- 重要なPOSTに冪等性キーがある
- 同時更新対策(ETag / If-Match)がある
- 長時間処理は非同期化されている
運用
- バージョニング方針と廃止ポリシーが決まっている
- OpenAPI仕様書がある
- リクエストIDでログを追跡できる