api-contract-review
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI 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
快速参考:常见问题
| Issue | Symptom | Impact |
|---|---|---|
| Wrong HTTP verb | POST for idempotent operation | Confusion, caching issues |
| Missing versioning | | Breaking changes affect all clients |
| Entity leak | JPA entity in response | Exposes internals, N+1 risk |
| 200 with error | | Breaks error handling |
| Inconsistent naming | | Hard to learn API |
| 问题 | 症状 | 影响 |
|---|---|---|
| HTTP动词使用错误 | 使用POST执行幂等操作 | 造成混淆、缓存问题 |
| 缺少版本控制 | | 破坏性变更影响所有客户端 |
| 实体泄露 | 响应中返回JPA entity | 暴露内部实现,存在N+1查询风险 |
| 错误响应返回200状态码 | | 破坏错误处理逻辑 |
| 命名不一致 | | API难以学习和使用 |
HTTP Verb Semantics
HTTP动词语义
Verb Selection Guide
动词选择指南
| Verb | Use For | Idempotent | Safe | Request Body |
|---|---|---|---|---|
| GET | Retrieve resource | Yes | Yes | No |
| POST | Create new resource | No | No | Yes |
| PUT | Replace entire resource | Yes | No | Yes |
| PATCH | Partial update | No* | No | Yes |
| DELETE | Remove resource | Yes | No | Optional |
*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
策略对比
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | | Clear, easy routing | URL changes |
| Header | | Clean URLs | Hidden, harder to test |
| Query param | | Easy to add | Easy to forget |
| 策略 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URL路径 | | 清晰,路由简单 | URL会变更 |
| 请求头 | | URL简洁 | 隐藏版本信息,测试难度高 |
| 查询参数 | | 易于添加 | 容易被忽略 |
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
成功状态码
| Code | When to Use | Response Body |
|---|---|---|
| 200 OK | Successful GET, PUT, PATCH | Resource or result |
| 201 Created | Successful POST (created) | Created resource + Location header |
| 204 No Content | Successful DELETE, or PUT with no body | Empty |
| 状态码 | 使用场景 | 响应体 |
|---|---|---|
| 200 OK | 成功的GET、PUT、PATCH请求 | 资源或结果 |
| 201 Created | 成功的POST请求(创建资源) | 创建的资源 + Location请求头 |
| 204 No Content | 成功的DELETE请求,或无响应体的PUT请求 | 空 |
Error Codes
错误状态码
| Code | When to Use | Common Mistake |
|---|---|---|
| 400 Bad Request | Invalid input, validation failed | Using for "not found" |
| 401 Unauthorized | Not authenticated | Confusing with 403 |
| 403 Forbidden | Authenticated but not allowed | Using 401 instead |
| 404 Not Found | Resource doesn't exist | Using 400 |
| 409 Conflict | Duplicate, concurrent modification | Using 400 |
| 422 Unprocessable | Semantic error (valid syntax, invalid meaning) | Using 400 |
| 500 Internal Error | Unexpected server error | Exposing 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)
破坏性变更(同一版本中需避免)
| Change | Breaking? | Migration |
|---|---|---|
| Remove endpoint | Yes | Deprecate first, remove in next version |
| Remove field from response | Yes | Keep field, return null/default |
| Add required field to request | Yes | Make optional with default |
| Change field type | Yes | Add new field, deprecate old |
| Rename field | Yes | Support both temporarily |
| Change URL path | Yes | Redirect 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 (, not
/users)/getUsers - Plural for collections (, not
/users)/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:
- List all controllers:
find . -name "*Controller.java" - Sample 2-3 controllers for pattern analysis
- Check configuration once
@ExceptionHandler - 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:
- 列出所有控制器:
find . -name "*Controller.java" - 选取2-3个控制器进行模式分析
- 检查一次配置
@ExceptionHandler - 使用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]"