mcpField
A
api-contract-review

api-contract-review

github.com/decebalsテスト・品質761291 回インストール7 週間前に更新
リポジトリ

api-contract-review について

REST APIの正確性、一貫性、互換性を監査するスキルです。HTTPの意味、エンドポイント設計、レスポンス、エラー、後方互換性を確認できます。

主な機能

  1. 01HTTPメソッドとステータスコードの使い方を確認
  2. 02URLの命名とバージョニングをレビュー
  3. 03DTO、ページネーション、レスポンスの一貫性を確認
  4. 04エラー形式と情報露出を確認
  5. 05破壊的変更と非推奨化の必要性を特定

活用シーン

  1. 01リリース前のAPI変更レビュー
  2. 02プルリクエスト内のコントローラー変更の確認
  3. 03後方互換性の評価
  4. 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 OKGET、PUT、PATCH の成功リソースまたは結果
201 CreatedPOST の成功(作成済み)作成されたリソース + Location ヘッダー
204 No ContentDELETE の成功、またはボディなしの 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 の場合:

  1. すべてのコントローラーを一覧表示します:find . -name "*Controller.java"
  2. パターン分析のため、コントローラーを 2~3 個サンプルとして確認します
  3. @ExceptionHandler の設定を一度確認します
  4. 特定のアンチパターンを 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]"
    

コメント