mcpField
R
rest-api-conventions

rest-api-conventions

github.com/rrezartprebreza開発328133 回インストール6 日前に更新
リポジトリ

rest-api-conventions について

rest-api-conventions は、プロジェクト既存の API 契約を保ちながら REST エンドポイントを追加・レビューするスキルです。ステータスコード、URL、ページネーション、エラー処理、実装上の注意点をコーディングエージェントに示します。

主な機能

  1. 01既存のレスポンスとバージョン契約の維持
  2. 02HTTP ステータスと API 結果の対応
  3. 03URL とリソース階層の規約
  4. 04上限付きページネーションの設計
  5. 05既定の方針に沿ったエラー処理
  6. 06Spring API の典型的な問題の回避

活用シーン

  1. 01無関係な API 契約を変えずにエンドポイントを追加
  2. 02ステータスコードとリソース URL の設定
  3. 03ページ上限と安定したレスポンス形式の設定
  4. 04Spring コントローラーの例外処理のレビュー

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill rest-api-conventions

インストール後、Claude Code で/rest-api-conventionsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
examples/
bad-controller.java
good-controller.java
templates/
ApiResponse.java
GlobalExceptionHandler.java
PageResponse.java

詳細

REST API の規約

プロジェクトの契約

レスポンスの契約を決める前に、既存のコントローラー、テスト、OpenAPI を確認してください。 プロジェクトの ID、レスポンスの形式、バージョニング戦略を維持してください。ルートを追加する際に、 無関係なエンドポイントを移行しないでください。以下のエンベロープは、これを使用するプロジェクト向けの 任意の規約です。通常の成功 DTO も同様に有効です。204 レスポンスには本文がありません。

成功形式とエラー形式は独立しています。既存の成功エンベロープと RFC 9457 のエラー形式は共存できます。 一貫したエラーポリシーを 1 つ使用してください。以下のレガシーエラーの例を選ぶのは、 プロジェクトですでにその形式が求められている場合に限ります。

成功エンベロープの例:

{
  "success": true,
  "data": { },
  "error": null,
  "timestamp": "2026-04-13T10:00:00Z"
}

エラーレスポンス:

{
  "success": false,
  "data": null,
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "Order with id 123 not found",
    "details": []
  },
  "timestamp": "2026-04-13T10:00:00Z"
}

ApiResponse ラッパー

@JsonInclude(JsonInclude.Include.NON_NULL)
public record ApiResponse<T>(
    boolean success,
    T data,
    ApiError error,
    Instant timestamp
) {
    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(true, data, null, Instant.now());
    }

    public static <T> ApiResponse<T> error(String code, String message) {
        return new ApiResponse<>(false, null, new ApiError(code, message, List.of()), Instant.now());
    }
}

public record ApiError(String code, String message, List<String> details) {}

HTTP ステータスのマッピング

シナリオステータス
GET — 見つかった200
POST — リソースを作成201
PUT/PATCH — 更新200
DELETE — 削除204(本文なし)
バリデーション失敗400
未認証401
アクセス禁止403
見つからない404
競合(重複)409
未処理のサーバーエラー500

URL の規約

  • リソースには複数形の名詞を使用します:/orders、/users、/products
  • 複数語の場合はケバブケースを使用します:/order-items。/orderItems ではありません
  • パスでバージョニングします:/api/v1/orders
  • ネストしたリソースは最大 2 階層です:/orders/{id}/items ✅、/orders/{id}/items/{itemId}/notes ❌ — /order-item-notes/{id} のようにフラット化します
  • ID:既存の ID 型を維持してください。UUID は選択肢の 1 つですが、認可の仕組みではありません。
GET    /api/v1/orders              → list (paginated)
POST   /api/v1/orders              → create
GET    /api/v1/orders/{id}         → get one
PUT    /api/v1/orders/{id}         → full update
PATCH  /api/v1/orders/{id}         → partial update
DELETE /api/v1/orders/{id}         → delete
GET    /api/v1/orders/{id}/items   → nested resource

ページネーション

{
  "success": true,
  "data": {
    "content": [...],
    "page": 0,
    "size": 20,
    "totalElements": 150,
    "totalPages": 8,
    "last": false
  }
}

クエリパラメーター:?page=0&size=20&sort=createdAt,desc

コントローラーでは Spring Data の Pageable を使用します:

@GetMapping
public ApiResponse<PageResponse<OrderResponse>> list(Pageable pageable) {
    return ApiResponse.ok(PageResponse.from(orderService.findAll(pageable).map(OrderResponse::from)));
}

ページサイズに上限を設定してください。 Pageable をそのまま使うと、どのクライアントからも ?size=100000 を指定でき、 1 回のリクエストでテーブル全体がメモリに読み込まれるおそれがあります。Spring のデフォルト上限は 2000 ですが、 ほとんどの API にはまだ大きすぎます:

spring:
  data:
    web:
      pageable:
        default-page-size: 20
        max-page-size: 100   # requests above this are silently clamped

コンパイル済みの PageResponse は、JSON ページネーションの契約を固定します。 エンティティを DTO にマッピングするだけでは、Spring Data の PageImpl のシリアライズは安定しません。 API に適した形式であれば、Spring Data の org.springframework.data.web.PagedModel も選択肢です。 ソートフィールドを許可リストで検証し、一意でないソートには ID をタイブレーカーとして追加してください。

レガシーエンベロープ用の例外ハンドラー

// Extends ResponseEntityExceptionHandler: without it the Exception.class catch-all turns
// malformed JSON, 405, bad path variables and unknown URLs into 500. Full version, verified
// against Boot 3 and 4: templates/GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleNotFound(EntityNotFoundException ex) {
        return ResponseEntity.status(404).body(ApiResponse.error("NOT_FOUND", ex.getMessage()));
    }

    @ExceptionHandler(AccessDeniedException.class) // @PreAuthorize denials surface here, not in the filter
    public ResponseEntity<ApiResponse<Void>> handleAccessDenied(AccessDeniedException ex) {
        return ResponseEntity.status(403).body(ApiResponse.error("FORBIDDEN", "Access denied"));
    }

    @Override // an @ExceptionHandler for this type would clash with the superclass at startup
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        List<String> details = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage()).toList();
        return ResponseEntity.status(400).body(ApiResponse.validationError(details));
    }

    @Override // keep the framework's status + headers, return the envelope
    protected ResponseEntity<Object> handleExceptionInternal(
            Exception ex, Object body, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        return ResponseEntity.status(status).headers(headers)
            .body(ApiResponse.error("HTTP_" + status.value(), "Request could not be processed"));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGeneric(Exception ex) {
        return ResponseEntity.status(500).body(ApiResponse.error("INTERNAL_ERROR", "An unexpected error occurred"));
    }
}

注意点

  • エージェントが ResponseEntityExceptionHandler を継承しないアドバイスに Exception.class の包括的な catch-all を記述すると、不正な JSON、405、型が一致しないパス変数、未知の URL がすべて 500 になります
  • エージェントが新しいエンベロープを導入する — 既存の成功時の契約を維持してください。
  • エージェントが競合するエラーハンドラーを作る — プロジェクトで確立されているエラーの契約を使用してください。
  • エージェントがコントローラーに例外ハンドラーを配置する — 必ず @RestControllerAdvice を使用してください
  • エージェントがエンドポイントの追加時に ID 型を変更する — 既存の ID 方式を維持してください。
  • エージェントが上限のない Pageable を受け入れる — spring.data.web.pageable.max-page-size を設定してください。そうしないと、1 回のリクエストでテーブル全体が取得されるおそれがあります
  • エージェントが PageImpl を直接シリアライズする — エンティティを DTO にマッピングしてから、PageResponse でラップしてください。

コメント