AI摘要
前传:我们为什么需要这份指南?
我曾参与一个中型的微服务项目,大约有20个服务。最初,每个团队按照自己的理解设计API。结果,当我需要调用不同团队的接口时,发现自己像是在阅读不同的语言:
- 用户服务:
GET /api/v1/user/getUserInfo?id=123 - 订单服务:
POST /api/order/queryOrderList - 支付服务:
GET /payment-service/retrievePayment/123
同一个项目里,有的用单数名词,有的用复数;有的用动词,有的用名词;有的路径带api前缀,有的带服务名。当新同事加入时,光是学习这些不一致的API规范就需要一周时间。更糟糕的是,前端对接时经常抱怨:"你们后端自己都没统一,我怎么知道该调哪个?"
从那次经历开始,我意识到一套统一的API设计规范不是"锦上添花",而是"雪中送炭"。
第一章:RESTful的灵魂——你真的理解了吗?
1.1 别把REST当CRUD的简单包装
很多开发者认为RESTful就是给CRUD操作换个HTTP方法的外衣,于是写出了这样的代码:
// 反面教材:这只是CRUD的HTTP包装
@PostMapping("/api/user/create")
public User createUser(@RequestBody User user) { ... }
@GetMapping("/api/user/read/{id}")
public User getUser(@PathVariable Long id) { ... }
@PostMapping("/api/user/update")
public User updateUser(@RequestBody User user) { ... }
@PostMapping("/api/user/delete/{id}")
public void deleteUser(@PathVariable Long id) { ... }问题在哪?
- 在URI中使用动词(create/read/update/delete)
- 用POST方法处理所有操作
- 资源的状态变化不清晰
1.2 RESTful的核心:资源与状态转移
REST(Representational State Transfer)的核心思想是资源和状态转移。让我们重构上面的例子:
// 正确示例:基于资源的操作
@PostMapping("/api/users") // 创建用户 -> 往用户集合中添加资源
public User createUser(@RequestBody User user) { ... }
@GetMapping("/api/users/{id}") // 获取特定用户
public User getUser(@PathVariable Long id) { ... }
@PutMapping("/api/users/{id}") // 全量更新用户
public User updateUser(@PathVariable Long id, @RequestBody User user) { ... }
@PatchMapping("/api/users/{id}") // 部分更新用户(可选)
public User partialUpdateUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) { ... }
@DeleteMapping("/api/users/{id}") // 删除用户
public void deleteUser(@PathVariable Long id) { ... }关键理解:
- URI标识的是资源(用户),而不是操作
- HTTP方法表示对资源的操作类型
- 同一个URI,不同的HTTP方法可以有不同的含义
第二章:命名规范——让API自己会说话
2.1 资源命名:名词、复数、小写
糟糕的命名:
GET /getAllUsers
POST /createNewOrder
GET /fetchProductDetails/123问题诊断:
- 在URI中使用动词
- 命名不一致(getAll vs fetch)
- 单复数混用
优雅的命名:
GET /users # 获取用户列表
POST /users # 创建用户
GET /users/{id} # 获取特定用户
PUT /users/{id} # 更新用户
DELETE /users/{id} # 删除用户
GET /orders # 获取订单列表
POST /orders # 创建订单
GET /orders/{id} # 获取特定订单2.2 关联资源的表达
当一个资源从属于另一个资源时,如何表达?
案例:获取用户123的所有订单
方案A:嵌套路径(当订单生命周期完全依赖用户时)
GET /users/123/orders方案B:查询参数(当只是过滤条件时)
GET /orders?userId=123选择标准:
- 如果订单不能脱离用户存在(用户删了,订单也删了),用嵌套
- 如果订单可以独立存在,只是按用户过滤,用查询参数
2.3 避免的常见陷阱
// 陷阱1:在URI中暴露操作类型
@PostMapping("/api/searchUsers") // 不好
@GetMapping("/api/users?name=xxx") // 好
// 陷阱2:在URI中包含动词
@PostMapping("/api/convertToPDF") // 不好
// 更好的设计:将"转换"视为创建PDF资源
@PostMapping("/api/pdf-documents") // 好
// 陷阱3:使用奇怪的缩写
@GetMapping("/api/usr/{id}") // 不好
@GetMapping("/api/users/{id}") // 好第三章:HTTP方法——各司其职,不要乱来
3.1 每个方法都有明确的语义
| 方法 | 语义 | 是否幂等 | 是否安全 |
|---|---|---|---|
| GET | 获取资源 | 是 | 是 |
| POST | 创建资源 | 否 | 否 |
| PUT | 全量更新/创建资源 | 是 | 否 |
| PATCH | 部分更新资源 | 否 | 否 |
| DELETE | 删除资源 | 是 | 否 |
常见的错误用法:
// 错误:用GET执行有副作用的操作
@GetMapping("/api/users/{id}/disable")
public void disableUser(@PathVariable Long id) {
// 修改了用户状态,但用的是GET方法
userService.disableUser(id);
}
// 正确:将"禁用"视为更新操作
@PatchMapping("/api/users/{id}")
public User updateUser(@PathVariable Long id,
@RequestBody UserUpdateRequest request) {
// request中包含要更新的字段,如status=DISABLED
return userService.partialUpdate(id, request);
}3.2 POST vs PUT:创建还是更新?
POST的特点:
- 用于创建资源
- 客户端不知道资源ID(由服务器生成)
- 响应通常返回201 Created
@PostMapping("/api/users")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<User> createUser(@RequestBody @Valid UserCreateRequest request) {
User user = userService.createUser(request);
return ResponseEntity
.created(URI.create("/api/users/" + user.getId())) // 在Location头中返回资源URI
.body(user);
}PUT的特点:
- 用于全量更新现有资源
- 客户端知道资源ID
- 也可用于创建(如果客户端指定ID),但这不是常见用法
- 响应通常返回200 OK或204 No Content
@PutMapping("/api/users/{id}")
public User updateUser(@PathVariable Long id,
@RequestBody @Valid UserUpdateRequest request) {
return userService.updateUser(id, request);
}3.3 PATCH:部分更新的艺术
当只需要更新资源的几个字段时,PATCH比PUT更合适。
// PATCH请求体示例
{
"op": "replace",
"path": "/email",
"value": "new-email@example.com"
}
// 或者使用简化的JSON Merge Patch
{
"email": "new-email@example.com",
"phone": null // 设置phone为null
}实现示例:
@PatchMapping("/api/users/{id}")
public User partialUpdateUser(@PathVariable Long id,
@RequestBody JsonNode patches) {
// 使用自定义的逻辑处理部分更新
return userService.partialUpdate(id, patches);
}第四章:状态码——不要只用200和500
4.1 常用状态码及其含义
// 正确使用状态码
@GetMapping("/api/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(user))
.orElse(ResponseEntity.notFound().build()); // 404 Not Found
}
@PostMapping("/api/users")
public ResponseEntity<User> createUser(@RequestBody @Valid UserCreateRequest request) {
User user = userService.createUser(request);
return ResponseEntity
.created(URI.create("/api/users/" + user.getId())) // 201 Created
.body(user);
}4.2 业务错误如何表达?
糟糕的做法:所有错误都返回200
{
"code": 5001,
"message": "用户不存在"
}
// HTTP状态码却是200 OK,这会让HTTP客户端困惑好一点的做法:使用400系列状态码
@PostMapping("/api/orders")
public ResponseEntity<Order> createOrder(@RequestBody @Valid OrderCreateRequest request) {
try {
Order order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
} catch (InsufficientInventoryException e) {
// 库存不足是业务逻辑错误,不是客户端参数错误
// 但用400 Bad Request也不完全合适
throw new ResponseStatusException(HttpStatus.CADHED, e.getMessage());
}
}更好的做法:定义清晰的错误响应格式
// 统一的错误响应体
public class ErrorResponse {
private String code; // 业务错误码,如"INSUFFICIENT_INVENTORY"
private String message; // 用户友好的错误消息
private String detail; // 开发调试用的详细错误信息(仅开发环境返回)
private LocalDateTime timestamp;
private String path; // 请求路径
// 构造方法、getter、setter
}
// 全局异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InsufficientInventoryException.class)
public ResponseEntity<ErrorResponse> handleInsufficientInventory(
InsufficientInventoryException ex, WebRequest request) {
ErrorResponse error = ErrorResponse.builder()
.code("INSUFFICIENT_INVENTORY")
.message("商品库存不足")
.detail(ex.getMessage())
.timestamp(LocalDateTime.now())
.path(request.getDescription(false).replace("uri=", ""))
.build();
// 使用409 Conflict表示资源状态冲突
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationExceptions(
MethodArgumentNotValidException ex, WebRequest request) {
// 收集所有验证错误
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse error = ErrorResponse.builder()
.code("VALIDATION_FAILED")
.message("请求参数验证失败")
.detail(errors.toString())
.timestamp(LocalDateTime.now())
.path(request.getDescription(false).replace("uri=", ""))
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}第五章:请求与响应设计
5.1 分页——不要返回全部数据
没有分页的灾难:
@GetMapping("/api/orders")
public List<Order> getAllOrders() {
// 当订单有10万条时,这个接口会拖垮服务
return orderService.findAll();
}正确的分页设计:
// 分页请求参数
public class PageRequest {
private Integer page = 1; // 页码,从1开始
private Integer size = 20; // 每页大小
private String sortBy = "id"; // 排序字段
private String direction = "ASC"; // 排序方向
// 计算offset给数据库使用
public Integer getOffset() {
return (page - 1) * size;
}
}
// 分页响应
public class PageResponse<T> {
private List<T> content;
private PageMetadata metadata;
@Data
public static class PageMetadata {
private Integer page;
private Integer size;
private Long totalElements;
private Integer totalPages;
private Boolean hasNext;
private Boolean hasPrevious;
}
}
// 使用示例
@GetMapping("/api/orders")
public PageResponse<Order> getOrders(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "DESC") String direction) {
PageRequest pageRequest = new PageRequest(page, size, sortBy, direction);
return orderService.findPage(pageRequest);
}5.2 字段过滤——不要总是返回全部字段
问题: 不同的场景需要不同的字段组合
- 列表页只需要基本信息
- 详情页需要完整信息
- 关联查询可能只需要ID和名称
解决方案1:多个端点
@GetMapping("/api/users") // 返回精简信息
public List<UserSimple> getUsers() { ... }
@GetMapping("/api/users/{id}") // 返回详细信息
public UserDetail getUserDetail(@PathVariable Long id) { ... }解决方案2:查询参数控制字段
GET /api/users/{id}?fields=id,name,email实现示例:
@GetMapping("/api/users/{id}")
public Map<String, Object> getUser(
@PathVariable Long id,
@RequestParam(required = false) String fields) {
User user = userService.findById(id);
if (fields == null || fields.isEmpty()) {
return objectMapper.convertValue(user, Map.class);
}
// 根据fields参数过滤字段
return filterFields(user, fields.split(","));
}5.3 时间格式——统一处理时区
常见问题: 前端显示的时间和数据库存储的时间不一致
解决方案:统一使用ISO 8601格式
// 配置Jackson
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
// 统一使用ISO 8601格式
builder.simpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
// 设置时区为UTC
builder.timeZone(TimeZone.getTimeZone("UTC"));
// 日期序列化为字符串
builder.serializers(new LocalDateTimeSerializer(
DateTimeFormatter.ISO_LOCAL_DATE_TIME));
// 字符串反序列化为日期
builder.deserializers(new LocalDateTimeDeserializer(
DateTimeFormatter.ISO_LOCAL_DATE_TIME));
};
}
}
// DTO中使用
public class UserResponse {
private Long id;
private String name;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private LocalDateTime updatedAt;
}第六章:版本管理——如何优雅地演进
6.1 版本在哪里?
常见的版本策略:
URI路径版本(最常用)
/api/v1/users /api/v2/users查询参数版本
/api/users?version=1 /api/users?version=2请求头版本
GET /api/users Accept: application/vnd.myapp.v1+json
推荐使用URI路径版本,原因:
- 直观,易于理解
- 浏览器可直接访问测试
- 不同版本的API可以并行部署
6.2 如何设计向后兼容的变更?
不兼容的变更示例(需要升版本):
// v1: 返回用户全名
public class UserV1 {
private String fullName;
}
// v2: 拆分为姓和名(不兼容变更)
public class UserV2 {
private String firstName;
private String lastName;
}兼容的变更示例(可在同一版本内):
// 原始版本
public class User {
private String fullName;
// 增加新字段(向前兼容)
private String email;
// 弃用旧字段,但不立即删除
@Deprecated
private String oldField;
// 提供新旧字段的转换方法
public String getFirstName() {
if (fullName == null) return null;
return fullName.split(" ")[0];
}
}6.3 版本迁移策略
// 同时支持v1和v2
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
// v1版本的实现
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
// v2版本的实现
}
// 或者在同一个控制器中处理多个版本
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(params = "version=1")
public UserV1 getUserV1(@PathVariable Long id) { ... }
@GetMapping(params = "version=2")
public UserV2 getUserV2(@PathVariable Long id) { ... }
// 默认版本
@GetMapping
public UserV2 getUser(@PathVariable Long id) { ... }
}第七章:文档——别让API成为黑盒
7.1 代码即文档:使用Swagger/OpenAPI
@Configuration
@EnableOpenApi
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.OAS_30)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo())
.tags(
new Tag("用户管理", "用户相关操作"),
new Tag("订单管理", "订单相关操作")
);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("电商系统API文档")
.description("RESTful API设计示例")
.version("1.0")
.contact(new Contact("开发团队", null, "dev@example.com"))
.build();
}
}
// 在控制器中使用注解
@RestController
@RequestMapping("/api/users")
@Tag(name = "用户管理", description = "用户相关操作")
public class UserController {
@GetMapping("/{id}")
@Operation(summary = "获取用户详情", description = "根据ID获取用户详细信息")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功"),
@ApiResponse(responseCode = "404", description = "用户不存在")
})
public ResponseEntity<User> getUser(
@Parameter(description = "用户ID", required = true, example = "123")
@PathVariable Long id) {
// 实现逻辑
}
}7.2 编写接口设计文档
即使有Swagger,也应该有一个设计文档。模板示例:
# 用户服务API设计文档
## 1. 概述
- 服务名称:用户服务
- 版本:v1
- 基础路径:/api/v1
## 2. 资源设计
### 用户(User)
字段说明:
- id: 用户ID,自增长
- username: 用户名,唯一
- email: 邮箱
- status: 状态(ACTIVE/INACTIVE)
## 3. 接口详情
### 3.1 获取用户列表
- **端点**:GET /users
- **描述**:分页获取用户列表
- **查询参数**:
- page: 页码,默认1
- size: 每页大小,默认20
- sortBy: 排序字段,默认id
- direction: 排序方向,默认ASC
- **响应示例**:{
"content": [
{
"id": 1,
"username": "john_doe",
"email": "john@example.com"
}],
"metadata": {
"page": 1,
"size": 20,
"totalElements": 100,
"totalPages": 5,
"hasNext": true,
"hasPrevious": false}
}
## 4. 错误码
| 错误码 | HTTP状态码 | 描述 |
|--------|------------|------|
| USER_NOT_FOUND | 404 | 用户不存在 |
| USERNAME_EXISTS | 409 | 用户名已存在 |
第八章:实战:一个完整的API设计示例
让我们设计一个电商系统的订单API:
// 订单状态枚举
public enum OrderStatus {
PENDING, // 待支付
PAID, // 已支付
SHIPPED, // 已发货
DELIVERED, // 已送达
CANCELLED // 已取消
}
// 订单查询参数
@Data
public class OrderQuery {
private OrderStatus status;
private LocalDate startDate;
private LocalDate endDate;
private String customerName;
// 分页参数
private Integer page = 1;
private Integer size = 20;
// 排序
private String sortBy = "createdAt";
private String direction = "DESC";
}
// 订单创建请求
@Data
@Validated
public class OrderCreateRequest {
@NotNull
private Long customerId;
@NotEmpty
private List<OrderItemRequest> items;
@NotBlank
private String shippingAddress;
@Data
public static class OrderItemRequest {
@NotNull
private Long productId;
@Min(1)
private Integer quantity;
}
}
// 订单响应
@Data
public class OrderResponse {
private Long id;
private Long customerId;
private String orderNumber;
private BigDecimal totalAmount;
private OrderStatus status;
private String shippingAddress;
private List<OrderItemResponse> items;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private LocalDateTime createdAt;
@Data
public static class OrderItemResponse {
private Long productId;
private String productName;
private BigDecimal unitPrice;
private Integer quantity;
private BigDecimal subtotal;
}
}
// 订单控制器
@RestController
@RequestMapping("/api/v1/orders")
@Tag(name = "订单管理")
public class OrderController {
@GetMapping
@Operation(summary = "查询订单列表", description = "支持多条件分页查询")
public PageResponse<OrderResponse> queryOrders(@Valid OrderQuery query) {
return orderService.queryOrders(query);
}
@GetMapping("/{orderNumber}")
@Operation(summary = "获取订单详情")
public OrderResponse getOrder(@PathVariable String orderNumber) {
return orderService.getOrder(orderNumber);
}
@PostMapping
@Operation(summary = "创建订单")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<OrderResponse> createOrder(
@RequestBody @Valid OrderCreateRequest request) {
OrderResponse order = orderService.createOrder(request);
return ResponseEntity
.created(URI.create("/api/v1/orders/" + order.getOrderNumber()))
.body(order);
}
@PatchMapping("/{orderNumber}/status")
@Operation(summary = "更新订单状态")
public OrderResponse updateOrderStatus(
@PathVariable String orderNumber,
@RequestBody Map<String, Object> update) {
OrderStatus newStatus = OrderStatus.valueOf(
(String) update.get("status"));
return orderService.updateStatus(orderNumber, newStatus);
}
@PostMapping("/{orderNumber}/cancel")
@Operation(summary = "取消订单")
public OrderResponse cancelOrder(@PathVariable String orderNumber) {
return orderService.cancelOrder(orderNumber);
}
// 嵌套资源:订单项
@GetMapping("/{orderNumber}/items")
@Operation(summary = "获取订单项列表")
public List<OrderItemResponse> getOrderItems(
@PathVariable String orderNumber) {
return orderService.getOrderItems(orderNumber);
}
}总结:好API的标志
- 直观性:看一眼URL就知道要做什么
- 一致性:整个系统使用相同的设计模式
- 自描述性:请求和响应的含义清晰
- 可预测性:遵循行业惯例,不搞特殊化
- 灵活性:支持过滤、分页、字段选择等
- 兼容性:向前兼容,平滑升级
记住,API设计不是一次性工作,而是持续的过程。开始可能不完美,但有了统一的规范和持续的改进,你的API会变得越来越优雅、可维护。
最关键的,为API消费者(前端、移动端、其他服务)考虑,而不是只考虑自己实现的方便。毕竟,好的API设计,最终会让整个团队(包括你自己)的效率都得到提升。