| name | api-contract-review |
| description | Use when designing, reviewing, or validating REST API contracts, endpoints, request/response schemas, error handling, and HTTP conventions in Spring Boot applications. |
API Contract Review Skill
You are an expert in REST API design for Spring Boot applications. You validate API contracts for consistency, correctness, and adherence to RESTful conventions.
REST Conventions
Resource Naming
@RestController
@RequestMapping("/api/v1/employees")
public class EmployeeController {
@GetMapping
@GetMapping("/{id}")
@PostMapping
@PutMapping("/{id}")
@DeleteMapping("/{id}")
@GetMapping("/{id}/assignments")
}
@RequestMapping("/api/getEmployee")
@RequestMapping("/api/employee/delete/{id}")
@RequestMapping("/api/emp")
HTTP Status Codes
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public ResponseEntity<ProductResponse> getById(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
@PostMapping
public ResponseEntity<ProductResponse> create(
@Valid @RequestBody CreateProductRequest request) {
var product = productService.create(request);
var location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(product.id())
.toUri();
return ResponseEntity.created(location).body(product);
}
@PutMapping("/{id}")
public ResponseEntity<ProductResponse> update(
@PathVariable Long id,
@Valid @RequestBody UpdateProductRequest request) {
var product = productService.update(id, request);
return ResponseEntity.ok(product);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
productService.delete(id);
}
}
Consistent Error Responses
public record ApiError(
Instant timestamp,
int status,
String error,
String message,
String path,
List<FieldError> fieldErrors
) {
public record FieldError(String field, String message) {}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(
ResourceNotFoundException ex, HttpServletRequest request) {
var error = new ApiError(
Instant.now(),
404,
"Not Found",
ex.getMessage(),
request.getRequestURI(),
List.of()
);
return ResponseEntity.status(404).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(
MethodArgumentNotValidException ex, HttpServletRequest request) {
var fieldErrors = ex.getBindingResult().getFieldErrors().stream()
.map(fe -> new ApiError.FieldError(fe.getField(), fe.getDefaultMessage()))
.toList();
var error = new ApiError(
Instant.now(),
400,
"Bad Request",
"Validation failed",
request.getRequestURI(),
fieldErrors
);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGeneral(
Exception ex, HttpServletRequest request) {
var error = new ApiError(
Instant.now(),
500,
"Internal Server Error",
"An unexpected error occurred",
request.getRequestURI(),
List.of()
);
return ResponseEntity.status(500).body(error);
}
}
Pagination
@GetMapping
public ResponseEntity<Page<ProductResponse>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name,asc") String[] sort) {
var pageable = PageRequest.of(page, size, Sort.by(
Sort.Order.by(sort[0]).with(Sort.Direction.fromString(sort.length > 1 ? sort[1] : "asc"))
));
var products = productService.findAll(pageable);
return ResponseEntity.ok(products);
}
Request Validation
public record CreateProductRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
String name,
@NotNull(message = "Price is required")
@Positive(message = "Price must be positive")
BigDecimal price,
@Size(max = 500, message = "Description must not exceed 500 characters")
String description,
@NotNull(message = "Category is required")
Long categoryId
) {}
Review Checklist
When reviewing an API contract, verify:
- Resource names are plural nouns (not verbs)
- HTTP methods match the operation semantics
- Status codes are correct (201 for creation, 204 for delete, etc.)
- Error responses follow a consistent schema
- Request bodies include validation annotations
- Pagination is supported for list endpoints
- Responses never expose internal entity structure
- URLs are versioned (
/api/v1/...)
- Query parameters use camelCase
- Sensitive data is never included in URLs