REST API設計のベストプラクティス — エンジニアが知るべき10のルール
はじめに
REST API はモダンな Web アプリケーションの基盤です。フロントエンドとバックエンドの分離、マイクロサービス間の通信、外部サービスとの連携 — あらゆる場面で API 設計の良し悪しが開発体験とシステムの品質を左右します。
この記事では、REST API を設計する際に押さえるべき 10 のルール を、良い例・悪い例のコード付きで解説します。API を作る側だけでなく、使う側にとっても有用な知識です。
ルール 1: URL 設計はリソース中心にする
原則
URL はリソース(名詞)を表現し、操作(動詞)は HTTP メソッドで表現します。
良い例
GET /api/users # ユーザー一覧
GET /api/users/123 # ユーザー詳細
POST /api/users # ユーザー作成
PUT /api/users/123 # ユーザー更新
DELETE /api/users/123 # ユーザー削除
悪い例
GET /api/getUsers
POST /api/createUser
POST /api/deleteUser/123
GET /api/getUserById?id=123
URL 設計のポイント
| ルール | 例 |
|---|---|
| リソース名は複数形 | /users (not /user) |
| ケバブケースを使う | /user-profiles (not /userProfiles) |
| ネストは浅く保つ | /users/123/posts は OK、3段以上は避ける |
| クエリパラメータでフィルタ | /users?role=admin&status=active |
ネストの深さ
リソースの親子関係は 2 段階までが推奨です。3 段以上になる場合は、トップレベルのリソースとして独立させましょう。
# OK: 2段階
GET /users/123/posts
# 避ける: 3段階以上
GET /users/123/posts/456/comments/789
# 代わりに
GET /comments/789
GET /posts/456/comments
ルール 2: HTTP メソッドを正しく使う
各メソッドの意味
| メソッド | 用途 | 冪等性 | 安全性 |
|---|---|---|---|
| GET | リソースの取得 | 冪等 | 安全 |
| POST | リソースの作成 | 非冪等 | 非安全 |
| PUT | リソースの全体更新 | 冪等 | 非安全 |
| PATCH | リソースの部分更新 | 冪等 | 非安全 |
| DELETE | リソースの削除 | 冪等 | 非安全 |
冪等性(Idempotency)とは
同じリクエストを何度送っても結果が変わらないことを 冪等 と言います。
GET /users/123— 何度呼んでも同じユーザーが返る(冪等)DELETE /users/123— 1回目で削除、2回目以降は 404(結果は同じ=冪等)POST /users— 呼ぶたびに新しいユーザーが作られる(非冪等)
冪等性を理解しておくと、リトライ処理やエラーハンドリングの設計が的確になります。
PUT vs PATCH
- PUT: リソース全体を置き換える(送信しないフィールドはデフォルト値になる)
- PATCH: 指定したフィールドだけ更新する
// PUT /api/users/123 — 全体を置き換え
{
"name": "田中太郎",
"email": "tanaka@example.com",
"role": "admin"
}
// PATCH /api/users/123 — メールだけ更新
{
"email": "new-email@example.com"
}
実務では PATCH を使うケースが圧倒的に多い です。
ルール 3: ステータスコードを正しく返す
よく使うステータスコード
| コード | 意味 | 使う場面 |
|---|---|---|
| 200 | OK | GET/PUT/PATCH の成功 |
| 201 | Created | POST でリソース作成成功 |
| 204 | No Content | DELETE の成功(レスポンスボディなし) |
| 400 | Bad Request | リクエストの形式が不正 |
| 401 | Unauthorized | 認証が必要(未ログイン) |
| 403 | Forbidden | 認証済みだが権限がない |
| 404 | Not Found | リソースが存在しない |
| 409 | Conflict | リソースの競合(重複メールなど) |
| 422 | Unprocessable Entity | バリデーションエラー |
| 429 | Too Many Requests | レート制限超過 |
| 500 | Internal Server Error | サーバー内部エラー |
よくある間違い
何でも 200 で返す
// 悪い例: エラーなのに 200
HTTP/1.1 200 OK
{
"success": false,
"error": "User not found"
}
// 良い例: 適切なステータスコード
HTTP/1.1 404 Not Found
{
"type": "/errors/not-found",
"title": "User not found",
"detail": "No user exists with ID 123"
}
401 と 403 を混同する
- 401: そもそも誰なのかわからない(認証トークンがない/無効)
- 403: 誰かはわかったが、その操作は許可されていない
ルール 4: 認証はトークンベースで
Bearer Token 認証
GET /api/users HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
認証方式の比較
| 方式 | 適した場面 | セキュリティ |
|---|---|---|
| API Key | サーバー間通信、外部連携 | 中(漏洩リスクあり) |
| Bearer Token (JWT) | ユーザー認証が必要なAPI | 高 |
| OAuth 2.0 | サードパーティアクセス | 高 |
| Basic Auth | 開発環境のみ | 低 |
JWT の注意点
- 有効期限を短く設定する: アクセストークンは 15分〜1時間
- リフレッシュトークンを使う: アクセストークンの更新用
- Secret は環境変数で管理: コードに書かない
- ペイロードに機密情報を入れない: JWT は Base64 エンコードなので誰でもデコードできる
// JWT ペイロードの例
{
"sub": "user-123",
"role": "admin",
"exp": 1716400000,
"iat": 1716396400
}
ルール 5: ページネーションを実装する
一覧 API でページネーションなしにすると、データ量が増えたときにレスポンスが巨大になり、パフォーマンスが崩壊します。
オフセットベース
GET /api/users?page=2&per_page=20
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 20,
"total": 150,
"total_pages": 8
}
}
メリット: 実装が簡単、任意のページにジャンプ可能 デメリット: データの追加/削除で結果がずれる、大きなオフセットでパフォーマンス低下
カーソルベース
GET /api/users?cursor=eyJ0IjoiMjAyNi0wNS0yMiJ9&limit=20
{
"data": [...],
"pagination": {
"next_cursor": "eyJ0IjoiMjAyNi0wNS0yMSJ9",
"has_more": true
}
}
メリット: データの追加/削除に強い、大規模データでも高速 デメリット: 任意のページにジャンプできない
どちらを選ぶか
| 要件 | 推奨方式 |
|---|---|
| 管理画面のテーブル表示 | オフセットベース |
| SNS のタイムライン | カーソルベース |
| データ量が 10万件以上 | カーソルベース |
| ページ番号が必要 | オフセットベース |
ルール 6: エラーレスポンスを統一する
RFC 7807 (Problem Details) 形式
エラーレスポンスの標準フォーマットとして RFC 7807 が広く採用されています。
{
"type": "/errors/validation",
"title": "Validation Error",
"status": 422,
"detail": "The request body contains invalid fields.",
"instance": "/api/users",
"errors": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "name",
"message": "Name is required"
}
]
}
エラーレスポンス設計のポイント
- 全エラーで同じ構造にする: クライアント側の処理が楽になる
- 内部エラーの詳細を露出しない: スタックトレースや SQL クエリを返さない
- 解決方法を示す: 「何が間違っているか」だけでなく「どう直すか」を伝える
- ユニークなエラーコードを付ける: ドキュメントと対応させる
悪い例
// 内部情報の漏洩
{
"error": "Error at line 42 in UserService.java",
"sql": "SELECT * FROM users WHERE id = '1; DROP TABLE users;--'"
}
エラーメッセージにスタックトレースやSQLクエリを含めると、攻撃者にシステムの内部構造を教えることになります。
ルール 7: バージョニングで後方互換性を保つ
URL パスでバージョニング
GET /api/v1/users
GET /api/v2/users
メリット: 直感的、キャッシュが効きやすい デメリット: URL が変わるので全クライアントの更新が必要
ヘッダーでバージョニング
GET /api/users
Accept: application/vnd.example.v2+json
メリット: URL が変わらない デメリット: デバッグしにくい、CDN キャッシュの設定が複雑
実務での推奨
URL パス方式 が最も広く使われており、実装もシンプルです。迷ったらこちらを選びましょう。
バージョンアップのルール
- 破壊的変更があるときだけ バージョンを上げる
- フィールドの追加 は破壊的変更ではない(v1 のまま追加可能)
- フィールドの削除・型変更 は破壊的変更(v2 を作る)
- 旧バージョンの廃止は十分な猶予期間を設ける(最低 6 ヶ月)
ルール 8: レート制限で API を守る
なぜ必要か
レート制限がないと、悪意のあるリクエストやバグによる大量リクエストでサーバーがダウンします。
レスポンスヘッダーで残り回数を通知
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1716400000
制限超過時のレスポンス
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"type": "/errors/rate-limit",
"title": "Rate Limit Exceeded",
"detail": "You have exceeded the rate limit of 100 requests per minute.",
"retry_after": 60
}
レート制限の設計指針
| 対象 | 制限例 | 識別キー |
|---|---|---|
| 一般ユーザー | 100 req/min | API Key or IP |
| 認証済みユーザー | 1,000 req/min | ユーザーID |
| 管理者 | 10,000 req/min | ユーザーID |
| 未認証 | 20 req/min | IP アドレス |
実装方法
Token Bucket アルゴリズムが最も一般的です。Redis と組み合わせて実装するのが定番です。
# 概念的な Token Bucket の例
import time
import redis
r = redis.Redis()
def is_rate_limited(user_id: str, limit: int = 100, window: int = 60) -> bool:
key = f"rate_limit:{user_id}"
current = r.get(key)
if current is None:
r.setex(key, window, 1)
return False
if int(current) >= limit:
return True
r.incr(key)
return False
ルール 9: ドキュメントを自動生成する
OpenAPI (Swagger) を使う
API ドキュメントを手書きで管理するのは非現実的です。OpenAPI Specification でスキーマを定義し、ドキュメントを自動生成しましょう。
# openapi.yaml の例
openapi: 3.0.3
info:
title: User API
version: 1.0.0
paths:
/api/users:
get:
summary: ユーザー一覧を取得
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
default: 20
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
ドキュメントツールの選択肢
| ツール | 特徴 |
|---|---|
| Swagger UI | 定番、インタラクティブなAPI操作が可能 |
| Redoc | 美しいレイアウト、読みやすい |
| Stoplight | ビジュアルエディタ付き |
コードファーストアプローチ
OpenAPI YAML を手書きするのではなく、コードのアノテーションや型定義から自動生成する方法もあります。
- Python: FastAPI は自動で OpenAPI ドキュメントを生成
- TypeScript: tsoa, NestJS の Swagger モジュール
- Go: swaggo/swag
# FastAPI の例 — コードから自動生成
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="User API", version="1.0.0")
class User(BaseModel):
id: int
name: str
email: str
@app.get("/api/users", response_model=list[User])
async def get_users(page: int = 1, per_page: int = 20):
"""ユーザー一覧を取得する"""
...
FastAPI では、このコードを書くだけで /docs に Swagger UI が自動生成されます。
ルール 10: テストを自動化する
テストの種類
| テスト種類 | 対象 | ツール例 |
|---|---|---|
| 単体テスト | ビジネスロジック | pytest, Jest, Vitest |
| 統合テスト | API エンドポイント | supertest, httpx |
| コントラクトテスト | API スキーマの互換性 | Pact, Schemathesis |
| 負荷テスト | パフォーマンス | k6, Locust |
統合テストの例(Python + httpx)
import httpx
import pytest
BASE_URL = "http://localhost:8080/api"
class TestUserAPI:
def test_create_user(self):
response = httpx.post(f"{BASE_URL}/users", json={
"name": "テストユーザー",
"email": "test@example.com"
})
assert response.status_code == 201
data = response.json()
assert data["name"] == "テストユーザー"
assert "id" in data
def test_get_user(self):
response = httpx.get(f"{BASE_URL}/users/1")
assert response.status_code == 200
data = response.json()
assert "name" in data
assert "email" in data
def test_get_nonexistent_user(self):
response = httpx.get(f"{BASE_URL}/users/99999")
assert response.status_code == 404
def test_create_user_invalid_email(self):
response = httpx.post(f"{BASE_URL}/users", json={
"name": "テストユーザー",
"email": "invalid-email"
})
assert response.status_code == 422
def test_list_users_pagination(self):
response = httpx.get(f"{BASE_URL}/users?page=1&per_page=10")
assert response.status_code == 200
data = response.json()
assert "data" in data
assert "pagination" in data
assert len(data["data"]) <= 10
CI での自動テスト
# .github/workflows/api-test.yml
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: pytest tests/ -v
まとめ: 10 のルール一覧
| # | ルール | ポイント |
|---|---|---|
| 1 | URL はリソース中心 | 名詞 + 複数形、ケバブケース |
| 2 | HTTP メソッドを正しく使う | GET/POST/PUT/PATCH/DELETE の意味を守る |
| 3 | ステータスコードを正しく返す | 200以外も使う、401 vs 403 を区別 |
| 4 | 認証はトークンベース | Bearer Token + JWT が標準 |
| 5 | ページネーションを実装 | オフセット or カーソル |
| 6 | エラーレスポンスを統一 | RFC 7807 形式 |
| 7 | バージョニング | URL パス方式が定番 |
| 8 | レート制限 | 429 + Retry-After ヘッダー |
| 9 | ドキュメント自動生成 | OpenAPI + Swagger UI |
| 10 | テストの自動化 | 統合テスト + CI |
API 設計は「一度決めたら変えにくい」ものです。最初にこれらのルールを押さえておくことで、後から破壊的変更を加える必要性を減らし、API の利用者(未来の自分を含む)にとって使いやすい設計を維持できます。