4、REST API 设计实践:URL 命名规范、版本管理、分页与过滤、错误处理

聊到 REST API 设计,我见过太多团队踩坑了。说白了,API 就是你和外部世界的契约。契约写得好,大家合作愉快;写得烂,后期维护就是一场噩梦。今天我把几个核心要点掰开揉碎讲清楚。

4.1 URL 命名规范:别让接口名变成猜谜游戏

URL 命名这件事,我个人的习惯是「名词复数 + 层级关系」。为什么用名词?因为 REST 的核心是资源,资源就是名词。动词应该交给 HTTP 方法去表达。

核心原则:

  • 使用小写字母,单词间用连字符(-)分隔
  • 避免使用下划线(_),因为下划线在某些字体下容易被遮挡
  • 不要出现动词,比如 /getUser 就是典型的反模式
  • 层级关系用斜杠(/)表示,但不要超过三级

举个例子,我之前接手过一个项目,接口长这样:/api/getUserInfoById。你想想看,这跟 RPC 有什么区别?改完之后变成 /api/users/{id},清爽多了。

# 好的命名
GET    /api/users              # 获取用户列表
GET    /api/users/{id}         # 获取单个用户
POST   /api/users              # 创建用户
PUT    /api/users/{id}         # 更新用户
DELETE /api/users/{id}         # 删除用户

# 不好的命名
GET    /api/getUserList        # 动词,冗余
POST   /api/createUser         # 动词,冗余
GET    /api/user_info          # 下划线,不推荐

小技巧:对于关联资源,用嵌套表示。比如获取某个用户的订单:/api/users/{userId}/orders。但注意,嵌套不要超过两层,否则考虑用查询参数。

4.2 版本管理:给 API 留条后路

我记得有一次,我们团队上线了一个新功能,结果把老接口的返回字段改了。前端同学第二天上班发现页面全崩了……嗯,从那以后我再也不敢不做版本管理了。

版本管理常见的有三种方式,我列个表格对比一下:

方式 示例 优点 缺点
URL 路径 /api/v1/users 直观,容易缓存 URL 变长,版本升级需要改路径
请求头 Accept: application/vnd.myapp.v1+json URL 干净,符合 REST 理念 调试不方便,浏览器直接访问困难
查询参数 /api/users?version=1 实现简单 容易被忽略,缓存策略复杂

我个人习惯用 URL 路径方式。为什么?因为直观。你打开浏览器就能看到版本号,调试的时候不用翻请求头。但要注意,版本号建议用 v1、v2 这样的整数,不要用 v1.0.1 这种小版本号——API 版本和软件版本是两码事。

注意:不要轻易废弃旧版本。我建议至少保留两个大版本,给客户端留足迁移时间。比如 v1 和 v2 同时运行,等 v2 稳定运行三个月后再考虑下线 v1。

4.3 分页与过滤:别让用户一次拿太多

你有没有遇到过这种情况?调一个接口,返回了 10 万条数据,浏览器直接卡死。其实这就是没做分页的后果。分页不是可选项,是必选项。

我常用的分页参数设计如下:

GET /api/users?page=1&size=20

返回格式:

{
  "data": [...],
  "pagination": {
    "page": 1,
    "size": 20,
    "total": 156,
    "totalPages": 8
  }
}

这里有个细节:total 字段要不要返回?我的建议是返回。虽然每次查询总数会有性能开销,但前端需要它来渲染分页组件。如果数据量实在太大(比如百万级),可以考虑用 hasMore 布尔值替代。

过滤参数怎么设计?我见过有人把过滤条件塞进 URL 路径里,比如 /api/users/active。这其实不好,因为「活跃用户」是一种状态,不是一种资源。正确的做法是用查询参数:

# 过滤
GET /api/users?status=active
GET /api/users?createdAfter=2024-01-01&createdBefore=2024-12-31
GET /api/users?search=keyword

# 排序
GET /api/users?sort=createdAt&order=desc

# 字段选择
GET /api/users?fields=id,name,email

避坑指南:我曾经遇到过一个接口,过滤参数名不统一,有的用 status,有的用 state,前端同学每次都要翻文档确认。后来我定了个规矩:所有过滤参数统一用 filter[字段名] 的格式,比如 filter[status]=active。这样既清晰又容易扩展。

4.4 错误处理:让错误信息真正有用

错误处理是 API 设计中最容易被忽视的部分。很多团队只返回一个 HTTP 状态码,比如 400 或 500,然后前端同学一脸懵逼:「到底哪里错了?」

我建议的错误响应格式长这样:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "用户 ID 为 12345 的记录不存在",
    "details": {
      "userId": "12345",
      "resourceType": "User"
    },
    "requestId": "req-abc-123-def"
  }
}

几个关键点:

  • code:机器可读的错误码,前端可以根据它做不同的处理
  • message:人类可读的错误描述,要具体,不要只说「参数错误」
  • details:额外的上下文信息,方便调试
  • requestId:请求追踪 ID,排查问题时特别有用

HTTP 状态码也要选对。我见过有人所有错误都返回 400,这其实不对。常用的状态码:

状态码 含义 使用场景
400 Bad Request 参数校验失败、请求格式错误
401 Unauthorized 未登录或 token 过期
403 Forbidden 已登录但无权限
404 Not Found 资源不存在
409 Conflict 数据冲突,比如重复创建
422 Unprocessable Entity 业务逻辑错误
429 Too Many Requests 限流触发
500 Internal Server Error 服务器内部错误

注意:永远不要把服务器内部的堆栈信息直接返回给客户端。我曾经见过一个接口,报错时直接把 Java 的 StackTrace 扔了出来,里面包含了数据库连接信息……这太危险了。内部错误记录到日志里就好,返回给客户端的 message 要经过脱敏处理。

4.5 知识体系总览

说了这么多,我画了一张图帮你梳理一下 REST API 设计的核心脉络:

REST API 设计 URL 命名规范 • 名词复数 + 层级 • 小写 + 连字符 • 避免动词 • 嵌套不超过两层 版本管理 • URL 路径方式 • 请求头方式 • 查询参数方式 • 保留两个大版本 分页与过滤 • page + size 分页 • 查询参数过滤 • 排序与字段选择 • 返回 total 或 hasMore 错误处理 • 统一错误格式 • 正确使用状态码 • 包含 requestId • 不暴露堆栈信息 一致性是关键 命名、版本、分页、错误 保持风格统一

你看,这四个方面其实都围绕一个核心——一致性。命名一致、版本策略一致、分页格式一致、错误结构一致。只要做到这四点,你的 API 就算不是最优雅的,也一定是最容易用的。

可可精品资料库