api-contract-review について
REST APIの正確性、一貫性、互換性を監査するスキルです。HTTPの意味、エンドポイント設計、レスポンス、エラー、後方互換性を確認できます。
主な機能
- 01HTTPメソッドとステータスコードの使い方を確認
- 02URLの命名とバージョニングをレビュー
- 03DTO、ページネーション、レスポンスの一貫性を確認
- 04エラー形式と情報露出を確認
- 05破壊的変更と非推奨化の必要性を特定
活用シーン
- 01リリース前のAPI変更レビュー
- 02プルリクエスト内のコントローラー変更の確認
- 03後方互換性の評価
- 04エンドポイントとエラーレスポンス設計の監査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ decebals/ claude-code-java --skill api-contract-reviewインストール後、Claude Code で/api-contract-reviewを入力して読み込まれたか確認します。
同梱ファイルREADME.md
詳細
API コントラクトレビューのスキル
REST API の設計を、正確性、一貫性、互換性の観点から監査します。
使用するタイミング
- ユーザーが「この API をレビューして」/「REST エンドポイントを確認して」と依頼した場合
- API の変更をリリースする前
- コントローラーの変更を含む PR をレビューする場合
- 後方互換性を確認する場合
クイックリファレンス:よくある問題
| 問題 | 症状 | 影響 |
|---|---|---|
| HTTP メソッドの誤り | 冪等な操作に POST を使用 | 混乱、キャッシュの問題 |
| バージョニングの欠如 | /v1/users ではなく /users | 破壊的変更がすべてのクライアントに影響 |
| エンティティの漏えい | レスポンスに JPA エンティティを含める | 内部情報の露出、N+1 のリスク |
| エラー時に 200 を返す | {"status": 200, "error": "..."} | エラー処理が機能しなくなる |
| 命名の不統一 | /getUsers と /users が混在 | API の習得が難しい |
HTTP メソッドのセマンティクス
メソッド選択ガイド
| メソッド | 用途 | 冪等性 | 安全性 | リクエストボディ |
|---|---|---|---|---|
| GET | リソースの取得 | あり | あり | なし |
| POST | 新しいリソースの作成 | なし | なし | あり |
| PUT | リソース全体の置き換え | あり | なし | あり |
| PATCH | 部分更新 | なし* | なし | あり |
| DELETE | リソースの削除 | あり | なし | 任意 |
*PATCH は実装によっては冪等になる場合があります。
よくある間違い
// ❌ POST for retrieval
@PostMapping("/users/search")
public List<User> searchUsers(@RequestBody SearchCriteria criteria) { }
// ✅ GET with query params (or POST only if criteria is very complex)
@GetMapping("/users")
public List<User> searchUsers(
@RequestParam String name,
@RequestParam(required = false) String email) { }
// ❌ GET for state change
@GetMapping("/users/{id}/activate")
public void activateUser(@PathVariable Long id) { }
// ✅ POST or PATCH for state change
@PostMapping("/users/{id}/activate")
public ResponseEntity<Void> activateUser(@PathVariable Long id) { }
// ❌ POST for idempotent update
@PostMapping("/users/{id}")
public User updateUser(@PathVariable Long id, @RequestBody UserDto dto) { }
// ✅ PUT for full replacement, PATCH for partial
@PutMapping("/users/{id}")
public User replaceUser(@PathVariable Long id, @RequestBody UserDto dto) { }
@PatchMapping("/users/{id}")
public User updateUser(@PathVariable Long id, @RequestBody UserPatchDto dto) { }
API のバージョニング
戦略
| 戦略 | 例 | メリット | デメリット |
|---|---|---|---|
| URL パス | /v1/users | 明確で、ルーティングが簡単 | URL が変わる |
| ヘッダー | Accept: application/vnd.api.v1+json | クリーンな URL | 目立たず、テストしにくい |
| クエリパラメーター | /users?version=1 | 簡単に追加できる | 忘れやすい |
推奨:URL パス
// ✅ Versioned endpoints
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { }
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 { }
// ❌ No versioning
@RestController
@RequestMapping("/api/users") // Breaking changes affect everyone
public class UserController { }
バージョニングのチェックリスト
- すべての公開 API のパスにバージョンが含まれている
- 内部 API は内部用と明記されている(または同様にバージョン管理されている)
- 古いバージョンの廃止戦略が定義されている
リクエスト/レスポンスの設計
DTO とエンティティ
// ❌ Entity in response (leaks internals)
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
// Exposes: password hash, internal IDs, lazy collections
}
// ✅ DTO response
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
User user = userService.findById(id);
return UserResponse.from(user); // Only public fields
}
レスポンスの一貫性
// ❌ Inconsistent responses
@GetMapping("/users")
public List<User> getUsers() { } // Returns array
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { } // Returns object
@GetMapping("/users/count")
public int countUsers() { } // Returns primitive
// ✅ Consistent wrapper (optional but recommended for large APIs)
@GetMapping("/users")
public ApiResponse<List<UserResponse>> getUsers() {
return ApiResponse.success(userService.findAll());
}
// Or at minimum, consistent structure:
// - Collections: always wrapped or always raw (pick one)
// - Single items: always object
// - Counts/stats: always object { "count": 42 }
ページネーション
// ❌ No pagination on collections
@GetMapping("/users")
public List<User> getAllUsers() {
return userRepository.findAll(); // Could be millions
}
// ✅ Paginated
@GetMapping("/users")
public Page<UserResponse> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.findAll(PageRequest.of(page, size));
}
HTTP ステータスコード
成功コード
| コード | 使用する場合 | レスポンスボディ |
|---|---|---|
| 200 OK | GET、PUT、PATCH の成功 | リソースまたは結果 |
| 201 Created | POST の成功(作成済み) | 作成されたリソース + Location ヘッダー |
| 204 No Content | DELETE の成功、またはボディなしの PUT | 空 |
エラーコード
| コード | 使用する場合 | よくある間違い |
|---|---|---|
| 400 Bad Request | 入力が無効、検証に失敗 | 「見つからない」場合に使用 |
| 401 Unauthorized | 認証されていない | 403 と混同 |
| 403 Forbidden | 認証済みだが許可されていない | 代わりに 401 を使用 |
| 404 Not Found | リソースが存在しない | 400 を使用 |
| 409 Conflict | 重複、同時更新の競合 | 400 を使用 |
| 422 Unprocessable | 意味上のエラー(構文は有効だが、意味が無効) | 400 を使用 |
| 500 Internal Error | 想定外のサーバーエラー | スタックトレースを露出 |
アンチパターン:エラーボディに 200 を返す
// ❌ NEVER DO THIS
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getUser(@PathVariable Long id) {
try {
User user = userService.findById(id);
return ResponseEntity.ok(Map.of("status", "success", "data", user));
} catch (NotFoundException e) {
return ResponseEntity.ok(Map.of( // Still 200!
"status", "error",
"message", "User not found"
));
}
}
// ✅ Use proper status codes
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
エラーレスポンスの形式
一貫したエラー構造
// ✅ Standard error response
public class ErrorResponse {
private String code; // Machine-readable: "USER_NOT_FOUND"
private String message; // Human-readable: "User with ID 123 not found"
private Instant timestamp;
private String path;
private List<FieldError> errors; // For validation errors
}
// In GlobalExceptionHandler
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.builder()
.code("RESOURCE_NOT_FOUND")
.message(ex.getMessage())
.timestamp(Instant.now())
.path(request.getRequestURI())
.build());
}
セキュリティ:内部情報を露出しない
// ❌ Exposes stack trace
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleAll(Exception ex) {
return ResponseEntity.status(500)
.body(ex.getStackTrace().toString()); // Security risk!
}
// ✅ Generic message, log details server-side
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
log.error("Unexpected error", ex); // Full details in logs
return ResponseEntity.status(500)
.body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred"));
}
後方互換性
破壊的変更(同じバージョンでは避ける)
| 変更 | 破壊的か | 移行方法 |
|---|---|---|
| エンドポイントの削除 | はい | 先に廃止予定とし、次のバージョンで削除 |
| レスポンスからフィールドを削除 | はい | フィールドを残し、null/デフォルト値を返す |
| リクエストに必須フィールドを追加 | はい | デフォルト値を設定して任意にする |
| フィールドの型を変更 | はい | 新しいフィールドを追加し、古いフィールドを廃止予定にする |
| フィールド名の変更 | はい | 一時的に両方をサポート |
| URL パスの変更 | はい | 古い URL から新しい URL へリダイレクト |
非破壊的変更(安全)
- リクエストに任意のフィールドを追加
- レスポンスにフィールドを追加
- 新しいエンドポイントを追加
- 新しい任意のクエリパラメーターを追加
廃止パターン
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@Deprecated
@GetMapping("/by-email") // Old endpoint
public UserResponse getByEmailOld(@RequestParam String email) {
return getByEmail(email); // Delegate to new
}
@GetMapping(params = "email") // New pattern
public UserResponse getByEmail(@RequestParam String email) {
return userService.findByEmail(email);
}
}
API レビューチェックリスト
1. HTTP のセマンティクス
- GET は取得のみに使用する(副作用なし)
- POST は作成に使用する(201 + Location を返す)
- PUT は全体の置き換えに使用する(冪等)
- PATCH は部分更新に使用する
- DELETE は削除に使用する(冪等)
2. URL 設計
- バージョン管理されている(
/v1/、/v2/) - 動詞ではなく名詞を使っている(
/getUsersではなく/users) - コレクションには複数形を使っている(
/userではなく/users) - 関係性を階層構造で表している(
/users/{id}/orders) - 命名規則が一貫している(kebab-case または camelCase のどちらかを選択)
3. リクエスト処理
-
@Validで検証している - 検証に失敗した場合のエラーメッセージが明確である
- リクエスト DTO を使用している(エンティティではない)
- 適切なサイズ制限を設けている
4. レスポンス設計
- レスポンス DTO を使用している(エンティティではない)
- エンドポイント間で構造が一貫している
- コレクションにページネーションを設けている
- 適切なステータスコードを使用している(エラーに 200 を返さない)
5. エラー処理
- エラー形式が一貫している
- 機械可読なエラーコードがある
- 人が読めるメッセージがある
- スタックトレースが公開されない
- 4xx と 5xx が適切に区別されている
6. 互換性
- 現行バージョンに破壊的変更がない
- 非推奨のエンドポイントが文書化されている
- 破壊的変更に対する移行手順がある
トークンの最適化
大規模な API の場合:
- すべてのコントローラーを一覧表示します:
find . -name "*Controller.java" - パターン分析のため、コントローラーを 2~3 個サンプルとして確認します
@ExceptionHandlerの設定を一度確認します- 特定のアンチパターンを grep で検索します:
# Find potential entity leaks grep -r "public.*Entity.*@GetMapping" --include="*.java" # Find 200 with error patterns grep -r "ResponseEntity.ok.*error" --include="*.java" # Find unversioned APIs grep -r "@RequestMapping.*api" --include="*.java" | grep -v "/v[0-9]"
コメント
Markdown 対応