api-contract-review

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Contract Review Skill

API契约审查技能

Audit REST API design for correctness, consistency, and compatibility.
审查REST API设计的正确性、一致性和兼容性。

When to Use

使用场景

  • User asks "review this API" / "check REST endpoints"
  • Before releasing API changes
  • Reviewing PR with controller changes
  • Checking backward compatibility

  • 用户询问"review this API" / "check REST endpoints"
  • 发布API变更前
  • 审查包含控制器变更的PR
  • 检查向后兼容性

Quick Reference: Common Issues

快速参考:常见问题

IssueSymptomImpact
Wrong HTTP verbPOST for idempotent operationConfusion, caching issues
Missing versioning
/users
instead of
/v1/users
Breaking changes affect all clients
Entity leakJPA entity in responseExposes internals, N+1 risk
200 with error
{"status": 200, "error": "..."}
Breaks error handling
Inconsistent naming
/getUsers
vs
/users
Hard to learn API

问题症状影响
HTTP动词使用错误使用POST执行幂等操作造成混淆、缓存问题
缺少版本控制
/users
而非
/v1/users
破坏性变更影响所有客户端
实体泄露响应中返回JPA entity暴露内部实现,存在N+1查询风险
错误响应返回200状态码
{"status": 200, "error": "..."}
破坏错误处理逻辑
命名不一致
/getUsers
/users
并存
API难以学习和使用

HTTP Verb Semantics

HTTP动词语义

Verb Selection Guide

动词选择指南

VerbUse ForIdempotentSafeRequest Body
GETRetrieve resourceYesYesNo
POSTCreate new resourceNoNoYes
PUTReplace entire resourceYesNoYes
PATCHPartial updateNo*NoYes
DELETERemove resourceYesNoOptional
*PATCH can be idempotent depending on implementation
动词使用场景幂等性安全性请求体
GET获取资源
POST创建新资源
PUT替换整个资源
PATCH部分更新否*
DELETE删除资源可选
*PATCH的幂等性取决于具体实现

Common Mistakes

常见错误示例

java
// ❌ 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) { }

java
// ❌ 使用POST执行查询操作
@PostMapping("/users/search")
public List<User> searchUsers(@RequestBody SearchCriteria criteria) { }

// ✅ 使用GET+查询参数(仅当查询条件非常复杂时才使用POST)
@GetMapping("/users")
public List<User> searchUsers(
    @RequestParam String name,
    @RequestParam(required = false) String email) { }

// ❌ 使用GET修改状态
@GetMapping("/users/{id}/activate")
public void activateUser(@PathVariable Long id) { }

// ✅ 使用POST或PATCH修改状态
@PostMapping("/users/{id}/activate")
public ResponseEntity<Void> activateUser(@PathVariable Long id) { }

// ❌ 使用POST执行幂等更新
@PostMapping("/users/{id}")
public User updateUser(@PathVariable Long id, @RequestBody UserDto dto) { }

// ✅ 使用PUT进行全量替换,使用PATCH进行部分更新
@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 Versioning

API版本控制

Strategies

策略对比

StrategyExampleProsCons
URL path
/v1/users
Clear, easy routingURL changes
Header
Accept: application/vnd.api.v1+json
Clean URLsHidden, harder to test
Query param
/users?version=1
Easy to addEasy to forget
策略示例优点缺点
URL路径
/v1/users
清晰,路由简单URL会变更
请求头
Accept: application/vnd.api.v1+json
URL简洁隐藏版本信息,测试难度高
查询参数
/users?version=1
易于添加容易被忽略

Recommended: URL Path

推荐方案:URL路径

java
// ✅ 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 { }
java
// ✅ 带版本的端点
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { }

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 { }

// ❌ 无版本控制
@RestController
@RequestMapping("/api/users")  // 破坏性变更会影响所有客户端
public class UserController { }

Version Checklist

版本控制检查清单

  • All public APIs have version in path
  • Internal APIs documented as internal (or versioned too)
  • Deprecation strategy defined for old versions

  • 所有公开API的路径中包含版本号
  • 内部API标记为内部(或同样进行版本控制)
  • 定义旧版本的废弃策略

Request/Response Design

请求/响应设计

DTO vs Entity

DTO vs Entity

java
// ❌ 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
}
java
// ❌ 响应中返回Entity(暴露内部实现)
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
    return userRepository.findById(id).orElseThrow();
    // 暴露内容:密码哈希、内部ID、懒加载集合
}

// ✅ 使用DTO返回响应
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    return UserResponse.from(user);  // 仅返回公开字段
}

Response Consistency

响应一致性

java
// ❌ 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 }
java
// ❌ 响应格式不一致
@GetMapping("/users")
public List<User> getUsers() { }  // 返回数组

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { }  // 返回对象

@GetMapping("/users/count")
public int countUsers() { }  // 返回原始类型

// ✅ 使用统一包装器(大型API推荐使用)
@GetMapping("/users")
public ApiResponse<List<UserResponse>> getUsers() {
    return ApiResponse.success(userService.findAll());
}

// 或者至少保持结构一致:
// - 集合:始终包装或始终返回原始数组(二选一)
// - 单个资源:始终返回对象
// - 统计数据:始终返回对象 { "count": 42 }

Pagination

分页处理

java
// ❌ 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));
}

java
// ❌ 集合接口未实现分页
@GetMapping("/users")
public List<User> getAllUsers() {
    return userRepository.findAll();  // 可能返回数百万条数据
}

// ✅ 实现分页
@GetMapping("/users")
public Page<UserResponse> getUsers(
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "20") int size) {
    return userService.findAll(PageRequest.of(page, size));
}

HTTP Status Codes

HTTP状态码

Success Codes

成功状态码

CodeWhen to UseResponse Body
200 OKSuccessful GET, PUT, PATCHResource or result
201 CreatedSuccessful POST (created)Created resource + Location header
204 No ContentSuccessful DELETE, or PUT with no bodyEmpty
状态码使用场景响应体
200 OK成功的GET、PUT、PATCH请求资源或结果
201 Created成功的POST请求(创建资源)创建的资源 + Location请求头
204 No Content成功的DELETE请求,或无响应体的PUT请求

Error Codes

错误状态码

CodeWhen to UseCommon Mistake
400 Bad RequestInvalid input, validation failedUsing for "not found"
401 UnauthorizedNot authenticatedConfusing with 403
403 ForbiddenAuthenticated but not allowedUsing 401 instead
404 Not FoundResource doesn't existUsing 400
409 ConflictDuplicate, concurrent modificationUsing 400
422 UnprocessableSemantic error (valid syntax, invalid meaning)Using 400
500 Internal ErrorUnexpected server errorExposing stack traces
状态码使用场景常见错误
400 Bad Request输入无效、验证失败用于“资源不存在”场景
401 Unauthorized未认证与403混淆使用
403 Forbidden已认证但无权限使用401代替
404 Not Found资源不存在使用400代替
409 Conflict重复资源、并发修改冲突使用400代替
422 Unprocessable语义错误(语法合法但含义无效)使用400代替
500 Internal Error服务器意外错误暴露堆栈跟踪

Anti-Pattern: 200 with Error Body

反模式:错误响应返回200状态码

java
// ❌ 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());
}

java
// ❌ 绝对不要这样做
@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(  // 仍然返回200状态码!
            "status", "error",
            "message", "User not found"
        ));
    }
}

// ✅ 使用正确的状态码
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
    return userService.findById(id)
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

Error Response Format

错误响应格式

Consistent Error Structure

统一错误结构

java
// ✅ 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());
}
java
// ✅ 标准错误响应
public class ErrorResponse {
    private String code;        // 机器可读:"USER_NOT_FOUND"
    private String message;     // 人类可读:"ID为123的用户不存在"
    private Instant timestamp;
    private String path;
    private List<FieldError> errors;  // 用于验证错误
}

// 在全局异常处理器中
@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());
}

Security: Don't Expose Internals

安全注意:不要暴露内部实现

java
// ❌ 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"));
}

java
// ❌ 暴露堆栈跟踪
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleAll(Exception ex) {
    return ResponseEntity.status(500)
        .body(ex.getStackTrace().toString());  // 安全风险!
}

// ✅ 返回通用消息,详细信息记录在服务器端日志中
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
    log.error("Unexpected error", ex);  // 完整信息记录在日志中
    return ResponseEntity.status(500)
        .body(ErrorResponse.of("INTERNAL_ERROR", "发生了意外错误"));
}

Backward Compatibility

向后兼容性

Breaking Changes (Avoid in Same Version)

破坏性变更(同一版本中需避免)

ChangeBreaking?Migration
Remove endpointYesDeprecate first, remove in next version
Remove field from responseYesKeep field, return null/default
Add required field to requestYesMake optional with default
Change field typeYesAdd new field, deprecate old
Rename fieldYesSupport both temporarily
Change URL pathYesRedirect old to new
变更类型是否具有破坏性?迁移方案
删除端点先标记为废弃,在下一版本删除
从响应中移除字段保留字段,返回null或默认值
在请求中添加必填字段设置为可选字段并提供默认值
修改字段类型添加新字段,标记旧字段为废弃
重命名字段临时支持新旧两个字段
修改URL路径将旧路径重定向到新路径

Non-Breaking Changes (Safe)

非破坏性变更(安全)

  • Add optional field to request
  • Add field to response
  • Add new endpoint
  • Add new optional query parameter
  • 在请求中添加可选字段
  • 在响应中添加字段
  • 添加新端点
  • 添加新的可选查询参数

Deprecation Pattern

废弃模式示例

java
@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);
    }
}

java
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {

    @Deprecated
    @GetMapping("/by-email")  // 旧端点
    public UserResponse getByEmailOld(@RequestParam String email) {
        return getByEmail(email);  // 委托给新实现
    }

    @GetMapping(params = "email")  // 新模式
    public UserResponse getByEmail(@RequestParam String email) {
        return userService.findByEmail(email);
    }
}

API Review Checklist

API审查检查清单

1. HTTP Semantics

1. HTTP语义

  • GET for retrieval only (no side effects)
  • POST for creation (returns 201 + Location)
  • PUT for full replacement (idempotent)
  • PATCH for partial updates
  • DELETE for removal (idempotent)
  • GET仅用于查询(无副作用)
  • POST用于创建资源(返回201 + Location头)
  • PUT用于全量替换(幂等)
  • PATCH用于部分更新
  • DELETE用于删除资源(幂等)

2. URL Design

2. URL设计

  • Versioned (
    /v1/
    ,
    /v2/
    )
  • Nouns, not verbs (
    /users
    , not
    /getUsers
    )
  • Plural for collections (
    /users
    , not
    /user
    )
  • Hierarchical for relationships (
    /users/{id}/orders
    )
  • Consistent naming (kebab-case or camelCase, pick one)
  • 已版本化(
    /v1/
    ,
    /v2/
  • 使用名词而非动词(
    /users
    ,而非
    /getUsers
  • 集合使用复数形式(
    /users
    ,而非
    /user
  • 按层级表示资源关系(
    /users/{id}/orders
  • 命名一致(选择短横线命名或驼峰命名,保持统一)

3. Request Handling

3. 请求处理

  • Validation with
    @Valid
  • Clear error messages for validation failures
  • Request DTOs (not entities)
  • Reasonable size limits
  • 使用
    @Valid
    进行验证
  • 验证失败时返回清晰的错误信息
  • 使用请求DTO(而非Entity)
  • 设置合理的大小限制

4. Response Design

4. 响应设计

  • Response DTOs (not entities)
  • Consistent structure across endpoints
  • Pagination for collections
  • Proper status codes (not 200 for errors)
  • 使用响应DTO(而非Entity)
  • 所有端点的响应结构一致
  • 集合接口实现分页
  • 使用正确的状态码(错误场景不返回200)

5. Error Handling

5. 错误处理

  • Consistent error format
  • Machine-readable error codes
  • Human-readable messages
  • No stack traces exposed
  • Proper 4xx vs 5xx distinction
  • 错误格式统一
  • 包含机器可读的错误码
  • 包含人类可读的错误信息
  • 不暴露堆栈跟踪
  • 正确区分4xx和5xx状态码

6. Compatibility

6. 兼容性

  • No breaking changes in current version
  • Deprecated endpoints documented
  • Migration path for breaking changes

  • 当前版本无破坏性变更
  • 已废弃的端点有文档说明
  • 破坏性变更提供迁移路径

Token Optimization

优化审查效率

For large APIs:
  1. List all controllers:
    find . -name "*Controller.java"
  2. Sample 2-3 controllers for pattern analysis
  3. Check
    @ExceptionHandler
    configuration once
  4. Grep for specific anti-patterns:
    bash
    # 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]"
针对大型API:
  1. 列出所有控制器:
    find . -name "*Controller.java"
  2. 选取2-3个控制器进行模式分析
  3. 检查一次
    @ExceptionHandler
    配置
  4. 使用grep查找特定反模式:
    bash
    # 查找潜在的实体泄露问题
    grep -r "public.*Entity.*@GetMapping" --include="*.java"
    
    # 查找错误响应返回200的模式
    grep -r "ResponseEntity.ok.*error" --include="*.java"
    
    # 查找未版本化的API
    grep -r "@RequestMapping.*api" --include="*.java" | grep -v "/v[0-9]"