3. API 设计规范:从命名到响应,把规矩立在前头
API 设计这事儿,说白了就是定规矩。规矩定得好,前后端吵架就少;规矩定得烂,联调的时候你就等着哭吧。我做了这么多年 API,见过太多「随缘设计」的项目——URL 一会儿用复数一会儿用单数,状态码永远返回 200 然后 body 里塞个 error 字段。嗯,这种项目最后无一例外都重构了。
这一章我们就把 API 设计的几个核心规范掰开揉碎讲清楚。你照着做,至少能保证你的接口看起来像个正经系统。
3.1 URI 命名规范:让 URL 自己会说话
一个好的 URI,应该像一份清晰的快递单——收件人、物品、操作一目了然。我个人习惯遵循以下几个原则:
- 全小写 + 连字符:
/user-orders而不是/userOrders或/user_orders。大小写敏感带来的坑我踩过不止一次。 - 用名词复数表示资源:
/users、/orders、/products。动词应该交给 HTTP 方法去表达。 - 层级关系用斜杠:
/users/{id}/orders表示「某个用户的订单」,别搞成/getUserOrders这种 RPC 风格。 - 查询参数用于过滤/排序/分页:
/orders?status=paid&page=1&size=20,别把过滤条件塞进路径里。
推荐 vs 不推荐
| 推荐 | 不推荐 | 原因 |
|---|---|---|
/users/{id} | /getUserById | 动词冗余,资源语义不清晰 |
/orders?status=paid | /orders/paid | 过滤条件不应作为路径层级 |
/user-addresses | /userAddresses | 驼峰在 URL 中可读性差 |
我的小习惯:设计 URI 时先画资源关系图。比如「用户有订单,订单有商品」,对应的 URI 就是 /users/{uid}/orders/{oid}/items/{iid}。层级别超过三级,超过就该考虑拆分接口了。
3.2 请求方法选择:GET 不是万能的
我记得刚带团队那会儿,有个同事所有接口都用 POST,理由是「GET 有长度限制」。我当时就笑了——长度限制是表象,语义混乱才是大问题。
HTTP 方法的选择其实很简单,记住这张表就够了:
| 方法 | 语义 | 幂等 | 安全 | 典型场景 |
|---|---|---|---|---|
| GET | 查询资源 | 是 | 是 | 获取用户列表、查询订单详情 |
| POST | 创建资源 / 提交数据 | 否 | 否 | 创建用户、提交订单 |
| PUT | 全量替换资源 | 是 | 否 | 更新用户全部信息 |
| PATCH | 部分更新资源 | 否 | 否 | 修改用户手机号 |
| DELETE | 删除资源 | 是 | 否 | 删除用户、取消订单 |
避坑指南:我曾经在一个项目中看到有人用 GET 方法删除资源,理由是「前端方便」。结果爬虫一跑,数据库差点被清空。记住:有副作用的操作,永远不要用 GET。
为什么会这样?因为 GET 请求会被浏览器缓存、会被预加载、会被搜索引擎索引。你想想看,如果删除操作能被缓存,那后果有多可怕。
3.3 状态码设计:别让调用方猜谜
状态码是 API 和调用方之间的「暗号」。暗号要对得上,双方才不累。我见过最离谱的设计是——所有请求都返回 200,然后在 body 里放一个 {"code": 500, "message": "服务器炸了"}。这不是脱裤子放屁吗?
HTTP 状态码分五大类,我们实际开发中常用的就这几个:
- 2xx 成功:
200 OK:GET、PUT、PATCH 成功201 Created:POST 创建成功,记得在响应头里返回Location204 No Content:DELETE 成功,或者更新操作不需要返回 body
- 3xx 重定向:API 里用得少,但
301/302在资源迁移时有用 - 4xx 客户端错误:
400 Bad Request:参数校验失败,请求格式不对401 Unauthorized:没登录或 过期403 Forbidden:登录了但没权限404 Not Found:资源不存在409 Conflict:资源冲突,比如重复创建422 Unprocessable Entity:语义错误,比如邮箱格式不对
- 5xx 服务端错误:
500 Internal Server Error:未捕获的异常502 Bad Gateway:网关或代理层问题503 Service Unavailable:服务过载或维护中
状态码使用铁律:
- 不要用 200 包装错误,该 4xx 就 4xx,该 5xx 就 5xx
- 不要滥用 500,参数校验失败返回 400 而不是 500
- 不要返回 200 + 空 body 表示删除成功,用 204
3.4 请求与响应结构设计:统一就是最好的设计
API 的请求和响应结构,最怕的就是「百花齐放」。同一个系统里,有的接口返回 {"data": {...}},有的返回 {"result": {...}},还有的直接裸返回数组。调用方每次都要看文档才知道怎么取数据,你说累不累?
我个人习惯用一套统一的响应结构:
{
"code": 0, // 业务状态码,0 表示成功
"message": "success", // 可读的消息
"data": { // 真正的业务数据
"id": 123,
"name": "张三"
},
"traceId": "abc-123" // 链路追踪 ID,排查问题用
}
为什么不用 HTTP 状态码直接表示业务状态?因为有些场景 HTTP 状态码不够用。比如「订单已支付,不能取消」,HTTP 层面是 200,但业务层面需要告诉调用方「操作不允许」。这时候业务状态码就派上用场了。
请求结构建议:
- GET 请求:参数放 query string,复杂过滤条件用
q参数传 JSON - POST/PUT/PATCH:body 统一用 JSON,不要混用 form-data 和 JSON
- 分页参数统一:
page(从 1 开始)、size(默认 20,最大 100)
3.5 知识体系总览
下面这张图把本章的核心知识点串起来了。你照着这个框架去设计 API,基本不会出大问题。
3.6 版本控制:给 API 留条后路
API 一旦上线,就不可能永远不变。但你不能让调用方跟着你一起改。所以版本控制是必须的。
我推荐两种方式:
- URL 路径版本:
/v1/users、/v2/users。最直观,调用方一眼就知道用哪个版本。 - 请求头版本:
Accept: application/vnd.myapp.v1+json。更优雅,但调用方需要额外配置。
我曾经踩过的坑:有个项目用 query 参数传版本号 ?version=1,结果缓存系统把 v1 和 v2 的响应混在一起了。从那以后我再也不用 query 参数做版本控制。
版本号从 v1 开始,不要搞什么 v0.1、v1.0.0。小版本迭代用兼容性设计,大版本变更才升版本号。记住:向后兼容是 API 设计者的基本修养。