・約6分で読めます

REST API設計のベストプラクティス — エンジニアが知るべき10のルール


PRXサーバー国内シェアNo.1のレンタルサーバー。高速・安定・サポート充実で初心者にもおすすめ。
Xサーバー 公式サイト →

はじめに

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"
    }
  ]
}

エラーレスポンス設計のポイント

  1. 全エラーで同じ構造にする: クライアント側の処理が楽になる
  2. 内部エラーの詳細を露出しない: スタックトレースや SQL クエリを返さない
  3. 解決方法を示す: 「何が間違っているか」だけでなく「どう直すか」を伝える
  4. ユニークなエラーコードを付ける: ドキュメントと対応させる

悪い例

// 内部情報の漏洩
{
  "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 の利用者(未来の自分を含む)にとって使いやすい設計を維持できます。

関連記事