コンテンツにスキップ

REST APIエンドポイント設計:/active・/inactiveを分けるアンチパターンとフィルタリングパターン

概要

GET /api/v1/products/activeGET /api/v1/products/inactive という設計の問題点と、スケーラブルな代替パターンの解説。

詳細

問題のある設計

GET /api/v1/products/active
GET /api/v1/products/inactive

なぜ問題か:

  1. ステータスが増えるたびにエンドポイントが増える

    今後 /api/v1/products/archived, /api/v1/products/pending も追加?
    → エンドポイントが際限なく増える
    

  2. /active はリソースか?アクションか?曖昧

  3. REST はリソース指向(名詞)
  4. active は状態(属性)であり、リソースではない

  5. 複数条件でのフィルタができない

    「active かつ category=electronics」→ どのエンドポイントを使う?
    

正しい設計:クエリパラメータでフィルタリング

# ステータスでフィルタ
GET /api/v1/products?status=active
GET /api/v1/products?status=inactive
GET /api/v1/products?status=archived

# 複数条件を組み合わせられる
GET /api/v1/products?status=active&category=electronics&sort=-created_at

# 複数ステータスを指定
GET /api/v1/products?status=active,pending

例外:サブリソースはパスに含める

# これは正しい(active が独立したリソースの場合)
GET /api/v1/orders/{id}/items         # 注文の全アイテム
GET /api/v1/users/{id}/followers      # ユーザーのフォロワー

# ステータスはあくまでフィルタ
GET /api/v1/orders?status=completed

実装側でのバリデーション

// Go での実装例
func GetProducts(w http.ResponseWriter, r *http.Request) {
    status := r.URL.Query().Get("status")

    // 許可されたステータス値のバリデーション
    validStatuses := map[string]bool{
        "active": true, "inactive": true, "archived": true,
    }
    if status != "" && !validStatuses[status] {
        http.Error(w, "invalid status", http.StatusBadRequest)
        return
    }

    // フィルタリングしてDBから取得
    products, err := db.GetProducts(r.Context(), status)
    ...
}

なぜ重要か / いつ使うか

  • 新しい API エンドポイントを設計するとき
  • コードレビューで「このエンドポイント設計で良いか」を判断するとき
  • 「RESTful な API とは?」の面接問題への回答の一部として