请求与响应设计:常见数据格式、命名风格、分页与过滤

接口设计这事儿,说白了就是定规矩。规矩定得好,后端写得爽,前端调得也爽。规矩定得差,联调时就是你死我活。今天我们来聊聊请求和响应设计中最基础、也最容易扯皮的那几个问题:数据格式选哪个、字段名怎么起、分页过滤怎么搞。

常见数据格式: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(推荐) XML(遗留系统) 字段命名风格 snake_case(后端) camelCase(前端) 分页 & 过滤 游标分页(推荐) 参数化过滤 请求参数解析 & 校验 标准化响应返回 设计闭环:格式统一 → 命名规范 → 分页稳定 → 过滤可控 → 响应标准化

嗯,把上面这几点串起来,一个靠谱的请求与响应设计基本就成形了。数据格式统一用 JSON,字段名用 snake_case 加统一后缀,分页优先游标,过滤坚持参数化 + 白名单。这些不是高深理论,是踩坑踩出来的实践经验。你想想看,接口设计最怕什么?最怕每个接口风格都不一样,前端每次对接都要读文档猜逻辑。标准统一的接口,联调效率翻倍,线上问题也好排查。