RESTful API 设计:API版本控制、错误处理、分页、HATEOAS
说实话,RESTful API 设计这件事,很多团队一开始都不太当回事。我见过太多项目,API 说改就改,客户端说崩就崩。等到线上出问题了,大家才开始慌。
今天咱们聊聊四个核心话题:版本控制、错误处理、分页、HATEOAS。这四点做好了,你的 API 才能算得上「专业」。
1. API 版本控制
为什么要做版本控制?说白了,就是怕改接口的时候,把老客户端搞挂了。我早期参与过一个项目,API 没有版本号,后端一改字段名,前端直接白屏。嗯,那场面,至今难忘。
常见的版本控制策略
| 策略 | 示例 | 优缺点 |
|---|---|---|
| URL 路径版本 | /api/v1/users |
直观、易缓存;但 URL 会变 |
| 请求头版本 | Accept: application/vnd.myapp.v2+json |
URL 干净;但调试稍麻烦 |
| 查询参数版本 | /api/users?version=2 |
简单;容易污染缓存 |
我个人习惯用 URL 路径版本。为什么?因为一眼就能看出来当前用的是哪个版本,排查问题的时候省心。你想想看,线上出 bug 了,运维看一眼 URL 就知道是不是版本不匹配,多方便。
@RequestMapping("/api/v{version}") 来统一处理。
@RestController
@RequestMapping("/api/v1/users")
class UserControllerV1 {
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): UserDto {
// 返回 v1 版本的用户数据
}
}
@RestController
@RequestMapping("/api/v2/users")
class UserControllerV2 {
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): UserDtoV2 {
// 返回 v2 版本的用户数据,可能多了个字段
}
}
2. 错误处理
错误处理做得好不好,直接决定了前端同学会不会在心里骂你。我曾经见过一个 API,报错只返回 {"error": "something went wrong"}。你说这让人怎么排查?
好的错误处理,应该做到三点:
- 状态码准确:200 就是成功,400 就是客户端问题,500 就是服务端问题。别什么都返回 200。
- 错误信息可读:告诉调用方「哪里错了」和「怎么改」。
- 统一格式:所有错误返回同一个结构,方便客户端统一处理。
推荐错误响应结构
{
"status": 400,
"code": "INVALID_PARAMETER",
"message": "用户ID不能为空",
"details": [
{
"field": "userId",
"reason": "must not be null"
}
],
"timestamp": "2025-03-21T10:30:00Z",
"path": "/api/v1/users"
}
在 Kotlin 中,我习惯定义一个全局异常处理器:
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException::class)
fun handleBadRequest(ex: IllegalArgumentException, request: HttpServletRequest): ResponseEntity<ErrorResponse> {
val error = ErrorResponse(
status = 400,
code = "INVALID_PARAMETER",
message = ex.message ?: "请求参数错误",
path = request.requestURI
)
return ResponseEntity.badRequest().body(error)
}
@ExceptionHandler(Exception::class)
fun handleGeneralError(ex: Exception, request: HttpServletRequest): ResponseEntity<ErrorResponse> {
// 记录日志,避免泄露敏感信息
log.error("Unexpected error", ex)
val error = ErrorResponse(
status = 500,
code = "INTERNAL_ERROR",
message = "服务器内部错误,请稍后重试",
path = request.requestURI
)
return ResponseEntity.status(500).body(error)
}
}
3. 分页
分页这件事,看起来简单,但坑不少。我记得有一次,一个接口没有做分页,结果一次返回了 10 万条数据,前端直接卡死。从那以后,我所有列表接口都强制分页。
推荐的分页参数
| 参数 | 说明 | 示例 |
|---|---|---|
page |
页码,从 0 或 1 开始 | page=0 |
size |
每页条数 | size=20 |
sort |
排序字段和方向 | sort=createdAt,desc |
分页响应结构
{
"content": [ ... ], // 当前页数据
"page": 0,
"size": 20,
"totalElements": 156,
"totalPages": 8,
"first": true,
"last": false
}
在 Kotlin 中,配合 Spring Data 的 Page 接口,分页非常方便:
@GetMapping
fun listUsers(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int,
@RequestParam(defaultValue = "createdAt,desc") sort: String
): Page<UserDto> {
val pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
return userRepository.findAll(pageable).map { it.toDto() }
}
size=10000 的情况。建议在服务端做上限限制,比如最大 100 条。超出就返回 400 错误,或者直接截断。
4. HATEOAS
HATEOAS 这个词听起来很高大上,其实说白了就是:API 返回的数据里,带上下一步可以做什么的链接。让客户端不用硬编码 URL,而是跟着 API 的「指引」走。
举个例子,你查询一个用户,API 不光返回用户信息,还告诉你「这个用户可以修改」、「这个用户可以删除」。客户端看到链接,就知道能做什么。
HATEOAS 示例
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com",
"_links": {
"self": { "href": "/api/v1/users/1" },
"update": { "href": "/api/v1/users/1", "method": "PUT" },
"delete": { "href": "/api/v1/users/1", "method": "DELETE" },
"orders": { "href": "/api/v1/users/1/orders" }
}
}
在 Kotlin 中,可以用 Spring HATEOAS 库来实现:
import org.springframework.hateoas.RepresentationModel
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*
data class UserResource(
val id: Long,
val name: String,
val email: String
) : RepresentationModel<UserResource>()
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): UserResource {
val user = userService.findById(id)
val resource = UserResource(user.id, user.name, user.email)
resource.add(linkTo(methodOn(UserController::class.java).getUser(id)).withSelfRel())
resource.add(linkTo(methodOn(UserController::class.java).deleteUser(id)).withRel("delete"))
resource.add(linkTo(methodOn(OrderController::class.java).listOrdersByUser(id)).withRel("orders"))
return resource
}
知识体系总览
下面这张图,帮你把今天讲的内容串起来:
总结一下
今天聊了四个点:
- 版本控制:别让老客户端崩了,我推荐 URL 路径方式。
- 错误处理:统一格式,准确状态码,别暴露敏感信息。
- 分页:强制分页,服务端做上限限制。
- HATEOAS:让 API 自描述,客户端跟着链接走。
这些看起来都是细节,但正是这些细节,决定了你的 API 是「能用」还是「好用」。我在项目中踩过不少坑,希望你能少走弯路。