rest-api-conventions について
rest-api-conventions は、プロジェクト既存の API 契約を保ちながら REST エンドポイントを追加・レビューするスキルです。ステータスコード、URL、ページネーション、エラー処理、実装上の注意点をコーディングエージェントに示します。
主な機能
- 01既存のレスポンスとバージョン契約の維持
- 02HTTP ステータスと API 結果の対応
- 03URL とリソース階層の規約
- 04上限付きページネーションの設計
- 05既定の方針に沿ったエラー処理
- 06Spring API の典型的な問題の回避
活用シーン
- 01無関係な API 契約を変えずにエンドポイントを追加
- 02ステータスコードとリソース URL の設定
- 03ページ上限と安定したレスポンス形式の設定
- 04Spring コントローラーの例外処理のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rrezartprebreza/ spring-boot-skills --skill rest-api-conventionsインストール後、Claude Code で/rest-api-conventionsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/bad-controller.javagood-controller.javatemplates/ApiResponse.javaGlobalExceptionHandler.javaPageResponse.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 でラップしてください。