请求与响应设计:常见数据格式、命名风格、分页与过滤
接口设计这事儿,说白了就是定规矩。规矩定得好,后端写得爽,前端调得也爽。规矩定得差,联调时就是你死我活。今天我们来聊聊请求和响应设计中最基础、也最容易扯皮的那几个问题:数据格式选哪个、字段名怎么起、分页过滤怎么搞。
常见数据格式:JSON 与 XML
现在新项目基本没人用 XML 做接口数据格式了。我入行那会儿,XML 还是主流,每个接口返回一堆带命名空间的标签,解析起来费劲不说,光看报文就头皮发麻。个人习惯,只要不是对接遗留系统,一律用 JSON。
JSON 为什么好?简单,轻量,前端直接 JSON.parse() 完事。你看下面两份数据:
// JSON
{
"user_id": 1024,
"name": "张三",
"roles": ["admin", "editor"],
"created_at": "2024-03-15T10:30:00Z"
}
// XML
<?xml version="1.0" encoding="UTF-8"?>
<user>
<user_id>1024</user_id>
<name>张三</name>
<roles>
<role>admin</role>
<role>editor</role>
</roles>
<created_at>2024-03-15T10:30:00Z</created_at>
</user>
JSON 的数据量差不多只有 XML 的一半。带宽省了,解析速度也快。XML 在跨系统签名验证、XSLT 转换这些场景还有优势,但纯粹做 API 数据交换,JSON 是事实标准。
| 对比项 | JSON | XML |
|---|---|---|
| 数据体积 | 小(无冗余标签) | 大(标签重复) |
| 解析效率 | 高 | 较低 |
| 前端友好 | 原生支持 | 需额外解析 |
| Schema 支持 | JSON Schema | XSD(较成熟) |
| 适用场景 | REST / GraphQL / gRPC-json | SOAP / 遗留系统 |
我的建议:新接口统一用 JSON,除非对端系统只支持 XML。JSON 里日期格式固定用 ISO 8601,不要搞 "2024-03-15" 和 "03/15/2024 10:30:00" 混着来——曾经有个项目就因为日期格式不统一,线上查了三个小时的 bug。
字段命名风格:统一就是生产力
接口字段名用下划线 (snake_case) 还是驼峰 (camelCase)?这个问题我能跟你聊一下午。说实话,两种都没错,错的是不统一。我个人倾向后端接口用 snake_case,前端消费时再转换。
为什么会这样?你看数据库字段、Python / Ruby / 大部分后端配置,基本都是 snake_case。如果接口直接用 snake_case,减少一次命名转换,逻辑更清晰。但前端同学用 JavaScript,习惯 camelCase。解决办法?后端保持 snake_case 输出,前端框架(如 axios 拦截器)统一转换,或者后端在网关层做字段映射。
// 后端响应(snake_case)
{
"user_id": 1024,
"display_name": "张三",
"last_login_at": "2024-03-15T10:30:00Z"
}
// 前端转换后(camelCase)
{
"userId": 1024,
"displayName": "张三",
"lastLoginAt": "2024-03-15T10:30:00Z"
}
命名规则 checklist:
- 统一:整个团队、整个项目组,必须只有一个命名风格
- 可读:避免缩写(
usr_nm不如user_name) - 语义:布尔字段用
is_/has_/can_前缀(如is_active) - 时间:字段名带
_at表示时间点(如created_at),带_date表示日期
避坑指南:我曾经接手过一个项目,接口同时出现 userName、user_name、UserName 三种写法。联调时前端同事对着文档都要猜字段名。两天时间全组重构命名规范,教训深刻。命名不统一,接口就废了一半。
分页设计:不止是 limit 和 offset
接口数据量一大,分页就绕不开。最简单的分页是 ?page=1&size=20,但你在生产环境用用看——用户翻到第 1000 页,数据库扫过去几万行,响应时间直接爆炸。
分页有两种主流做法:基于偏移量的分页和基于游标的分页。
| 分页方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| offset / limit | OFFSET 100 LIMIT 20 | 实现简单,可跳页 | 深度分页性能差,数据变化时结果偏移 |
| cursor / keyset | WHERE id > last_id LIMIT 20 | 性能稳定,不分页深度影响 | 不能直接跳页,排序字段需唯一 |
| seeks | 基于复合索引 seek | 极高性能,适合大数据量 | 实现较复杂,排序限制多 |
举个例子,翻到第 5000 页,offset 分页是这样:SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 99980。数据库要先扫 99980 行,再扔掉。游标分页呢?SELECT * FROM users WHERE id > 99980 ORDER BY id LIMIT 20,走主键索引,直接定位,不管多少页性能都一样。
我建议:列表类接口(如订单列表、消息列表)用游标分页。翻页操作是「下一页 / 上一页」,不需要跳页。如果产品非要支持跳页(比如管理后台),折中方案是分页参数带 page 和 size,但限制最大页码为 100。
过滤设计:灵活但不失控
过滤跟分页经常一起出现。设计过滤参数有个核心原则:接口能做的过滤,别让客户端自己做。客户端拿到全量数据再过滤,不仅流量浪费,安全也是个问题。
常用的过滤参数风格:
// 单字段精确匹配
GET /api/users?status=active
// 多字段组合
GET /api/orders?status=paid&payment_method=alipay
// 范围过滤(时间、价格)
GET /api/orders?created_at_gt=2024-01-01&created_at_lt=2024-03-01
// 搜索(模糊匹配)
GET /api/users?search=张三
// 字段筛选(只返回指定字段)
GET /api/users?fields=id,name,email
过滤参数的命名也值得统一。常见的后缀约定:
_gt/_gte:大于 / 大于等于_lt/_lte:小于 / 小于等于_in:包含(如status_in=active,pending)_like:模糊查询_between:范围区间
小技巧:过滤参数一定要做白名单校验。客户端传 ?password=xxx 你不能真去查密码字段。做个允许过滤的字段列表 allowed_filter_fields,不在列表里的参数直接忽略或报错。
分页与过滤的响应结构,我推荐这样:
{
"data": [...],
"pagination": {
"cursor": "eyJpZCI6MTAyNH0=",
"has_more": true,
"total": null
},
"filters": {
"applied": ["status", "created_at_gt"],
"available": ["status", "payment_method", "created_at_gt", "created_at_lt"]
}
}
注意 total 在游标分页可以设为 null,因为很多场景下不需要精确总数。如果业务必须展示总数,可以用 EXPLAIN 估算或加缓存。
嗯,把上面这几点串起来,一个靠谱的请求与响应设计基本就成形了。数据格式统一用 JSON,字段名用 snake_case 加统一后缀,分页优先游标,过滤坚持参数化 + 白名单。这些不是高深理论,是踩坑踩出来的实践经验。你想想看,接口设计最怕什么?最怕每个接口风格都不一样,前端每次对接都要读文档猜逻辑。标准统一的接口,联调效率翻倍,线上问题也好排查。