API接口规范实战:从JSON约定到错误码的联调避坑指南

发布时间:2026/10/12 10:37:58
API接口规范实战:从JSON约定到错误码的联调避坑指南
简介这是一份面向后端开发与前后端协作团队的 API 接口设计规范文档针对接口路径混乱、参数不统一、返回格式不一致等常见问题给出了可落地的标准化约定。文档从实际项目视角展开涵盖 RESTful 设计原则、JSON 数据格式、日期金额格式、路径与版本号命名规则并详细规定 limit、offset、page、per_page、sort_by、order 等请求参数以及响应代码、数据列表、总条数等返回结构同时补充身份验证、权限控制、HTTPS 加密等安全性要求并对 200、400、401、403、404、405、500 等错误码的含义和处理方式做了说明。此外还包含驼峰命名、空值使用 [] 与 、避免 null 等细节约定。资源为单个 docx 文件压缩包约 20KB便于直接查阅与团队内部传阅。目前已有 3578 人学习适合正在制定或完善接口规范的中小型研发团队参考可直接对照条目评审现有接口设计减少前后端联调返工与维护成本。1. 接口规范文档先把后端团队的“打架条款”焊死这份《API接口设计规范.docx》是我见过最不像文档的文档——它没有空谈架构,而是直接把后端和前端最容易扯皮的九个条款写成了硬约定:JSON合法性谁负责、空值到底用[]还是、金额到底传不传千分位、时间格式是不是必须2019-01-01 12:12:12。换句话说,它是一份条款级契约,不是PPT级指南。适合的人群很明确:后端接口开发者、前端联调负责人、以及刚接管老接口项目想统一出口的维护者。你拿它当参考的价值,不在于“学RESTful”,而在于它把「谁说了算」这件事,在文档层面焊死了。2. 基础约定与数据类型九个条款里的四个决策点2.1 JSON 合法性归后端谁产错谁背锅文档开头第一条就把责任边界写死了:后端保证 JSON 格式合法性,前端不做格式合法性判断。这条约定在实战中的直接效果是——前端拿到的响应体,只要能JSON.parse就必须成功,不需要再写一层 try-catch 做兜底解析。我自己实际拆过的一个跨平台项目,当时前端每个请求都包了一层res.data.data的容错逻辑,后来后端统一了格式,前端直接删掉了 200 多行防御代码。这条约定的另外一个潜台词是:后端要对自己的序列化结果负责,Content-Type必须是application/json; charsetUTF-8。我一般会在网关层强制设置,防止个别接口漏配导致前端拿到text/html而触发下载行为。2.2 空值策略null 和 undefined 是最大的雷文档规定:空对象用[],空列表用[],空字符串用,默认数字用0,尽量避免 null 和 undefined。这看起来有点冗余——空对象和空列表都写[],但实际用意是前端拿到任何字段都能安全访问。你想象一下这个场景:用户列表接口,list字段在无数据时返回了null,前端data.list.length直接抛异常,这就是最常见的线上事故来源。我自己的习惯是在后端统一响应模型里做空值清洗,不让业务代码里散落的null直接透出。具体做法是:返回给前端的data里的集合字段,初始化时一律Collections.emptyList(),字符串字段一律,数字字段一律0,从源头杜绝null被序列化成field: null。2.3 key 命名与脱敏:驼峰是下限,*号是红线key 使用驼峰命名、首字母小写,这条规约本身不复杂,但实际执行时有一个容易翻车的细节:数据库字段名和接口 key 经常不一致。比如库字段是user_id,接口返回的是userId,如果后端直接返回 ORM 实体,key 名就漏了出去。所以规范里写“key 集合是后端返回的子集,即 key 不缺失”,意思是你可以多返回字段,但不能少前端依赖的字段。脱敏规则也写得很具体:手机号、身份证、邮箱、支付账号、邮寄地址等,返回时需要加*号。这块我踩过坑——用户管理后台接口把完整手机号返回给前端,虽然前端列表没有展示,但接口是开放的,最后被爬虫拉走了全量用户手机号。从那以后,凡是涉及隐私字段的接口,后端正则替换好再返回,永远是最后的底线。2.4 时间与金额:格式化字符串 vs 原始数字金额和时间格式,是这份规范里最容易引发歧义的两条。文档的原话是:金额以元为单位,显示性的后台返回格式化后的字符串(如6,800);时间以统一格式的字符串传递(如2019-01-01 12:12:12)。实战里要注意一个边界:给列表页展示用的数据和给图表计算的原始数据,应该分开。展示用6,800没问题,但如果前端要做求和、统计、导出 Excel,格式化后的字符串就只能重新解析——这就是典型的“显示层污染数据层”。我一般会建议在接口字段名上做区分,展示字段叫amount_display,计算字段叫amount(数字类型),前端各取所需。3. 请求路径与行为映射把 URL 写成可读的陈述句3.1 路径模板/版本/业务功能/表名/行为文档给出的路径范式是:path /{版本}/{具体的业务功能}/{表名}/{行为}示例:/api/v1/acs/users/ 访问所有用户资源 /api/v1/acs/users/1 访问 Id 为 1 的用户 /api/v1/acs/accounts/UpdateAllUserFlags 更新所有用户的某个标识这套设计的精妙之处在于行为段和表名段是分离的。比如/acs/users/group/iws-tech/minAge/30,语义就是“acs 业务下,users 表中,取 iws-tech 组且最小年龄 30 岁的用户”,路径本身就是一句可读的陈述句。我接手过一个模块,路径写的是/api/getUserInfoById和/api/users/getUserList混用,后来统一成/api/v1/users/get_by_id?id?之后,联调双方看一眼 URL 就知道在调哪个资源。3.2 HTTP 方法映射表:行为藏在 Path 里文档的请求方法映射是这套规范里值得细看的内容,我拿几个核心条目做下拆解:行为语义HTTP 方法Path 示例参数位置说明添加对象POST/addbody(JSON)接收前端 POST 的 JSON插入对象GET/insert?param?query与 add 等价,传参方式不同删除GET/delete_by_id?id?query逻辑删除,不物理删库更新POST/updatebody(JSON)前端提交整个对象更新GET/update_by_id?id?query按主键更新查询单个GET/get_by_id?id?query表名已指明实体,不必再写 pojo查询列表GET/list_by_param?param?page2per_page100query支持分页/排序/过滤看到这个表你可能会问:为什么插入、删除、更新都要提供 GET 版本和 POST 版本两套?我的理解是,这套规范的作者把 GET 版本当作“轻量级调试入口”来设计的——在浏览器地址栏直接就能触发操作,方便后端自测和前端临时验证。但生产环境我强烈建议只开放 POST 版本,GET 请求会进服务器访问日志,URL 里带敏感参数等于裸奔,而且某些网关或 CDN 会缓存 GET 响应。3.3 分页与排序参数:limit、offset、page、sort_by 怎么搭规范文档里定义了四个通用查询参数,实际使用时要先想清楚一件事——你这个接口到底用游标分页还是偏移分页?limit10 # 返回 10 条记录 offset10 # 从第 10 条之后开始取 page2per_page100 # 第 2 页,每页 100 条 sort_bynameorderasc # 按 name 升序page/per_page和limit/offset是两种分页风格。文档里两者都保留了,这是一步好棋,因为有的前端组件库(比如老版本的分页表格)只认 page/per_page,后端加一个参数转换层就行:// 参数解析层:page/per_page 与 limit/offset 互转 public PageParam convert(Integer page, Integer perPage, Integer limit, Integer offset) { PageParam result new PageParam(); if (page ! null perPage ! null) { result.setLimit(perPage); result.setOffset((page - 1) * perPage); } else if (limit ! null offset ! null) { result.setLimit(limit); result.setOffset(offset); } else { result.setLimit(20); // 默认每页 20 条 result.setOffset(0); } return result; }这里有个隐藏的坑:排序字段必须做白名单校验。如果直接把前端传来的sort_by拼进 SQL,等于把排序字段变成了注入入口。我一般在服务端维护一个MapString, String白名单,只允许name - user_name这类映射参与排序。4. 响应体与错误码设计统一的壳子,不统一的壳子就会出事4.1 标准响应体结构code、success、message、data、timestamp文档给出的响应体结构是:{ code: 200, success: true, message: 操作成功, data: { list: [], total: 0 }, timestamp: 1717171717171 }这个壳子的设计意图很明确——前端的 Axios 拦截器只认这一套结构。success是业务成功与否,code是具体错误码,message是人话提示,data是真正的数据载体。后端只要保证这个壳子不变,前端就能全局统一处理错误弹窗、加载态卸载和成功态跳转。我在实际项目中会额外要求:data字段要么有值,要么省略,绝不能给 null。前端如果写res.data.data.list,一旦后端哪天返回了data: null,这行代码直接崩掉。文档里ResultT的实体类其实已经规避了这个问题——它默认data为 null,但实际返回时ok(data)会塞值,error()时data不参与序列化,前端看successfalse走不了取值分支。4.2 响应码枚举:400/401/403/404/405/500 的分工文档给的 ResultCodeEnum 里有七个码,我把语义边界按自己的经验补完整:响应码语义触发场景前端处理建议200操作成功正常返回直接消费 data400请求格式或参数不对缺参、参数类型错、JSON 解析失败提示「请求参数错误」,不重试401未授权未携带 token 或 token 过期跳登录页403参数校验失败参数格式合法但业务规则不允许(如密码太短)展示 message 具体原因404地址未找到路径错误检查接口地址是否拼错405不支持的 HTTP 方法用 POST 调了只支持 GET 的接口检查方法配置500内部程序错误后端异常提示「系统繁忙」,可重试一次有个细节值得注意:文档把 403 定义为参数验证错误,而不是权限不足。这是国内团队很常见的做法,因为权限不足通常用 401 兼职兜底,403 专门留给参数校验。如果你接手的项目是这套规范,前端就不能把 403 当作“无权限”处理。4.3 ResultT 的 Java 实现:静态方法直接落地文档贴的ResultT类是我见过最实用的响应体封装,它把ok、error、missingArgument、validException这些静态工厂方法都定义好了,后端新写一个接口时只需要:PostMapping(/add) public ResultObject add(RequestBody User user) { if (user.getName() null || user.getName().isEmpty()) { return Result.missingArgument(用户名不能为空); } boolean saved userService.save(user); if (saved) { return Result.ok(user.getId()); } return Result.error(保存用户失败); }这段代码有两个值得注意的点。第一,missingArgument返回 400,error(String)默认走 500,这和文档的响应码映射完全一致。第二,ok(id)把新用户 ID 返回给前端,前端可以直接拿这个 ID 跳详情页或者刷新列表。我在实际项目里会把Result放在独立的common模块里,各业务服务之间不允许各自重新定义 ResponseBody,否则文档写了等于白写——壳子不统一,前端拦截器就变成筛子。5. 接口规范落地避坑:五个典型的联调事故复盘5.1 空对象返回了null,前端直接白屏现象:用户详情接口在查询不到数据时返回了data: null,前端this.userInfo res.data.data后页面直接报Cannot read properties of null。原因:后端用Result.ok(null)返回,data序列化为null,没有走文档约定的空对象策略。解决:后端做响应体统一拦截,对data为空的情况强制转成{}或[]。我一般会在Result.ok()的重载里做判断:如果传入的对象是 null,新建空 JSONObject 占位,保证data永远是一个可访问的对象。5.2 时间格式不统一,前端排序全乱现象:同一个接口里,创建时间返回2024-01-01 12:00:00,更新时间返回时间戳1704072000,前端列表页的时间排序完全不按预期。原因:两个开发同事各自用了不同的时间处理工具,一个格式化成了字符串,一个直接透传了数据库的时间戳字段。解决:按文档约定,接口统一返回yyyy-MM-dd HH:mm:ss字符串。我通常是后端的 Jackson 全局配置里注册一个自定义时间序列化器,把LocalDateTime统一序列化成文档要求的格式,从出口上杜绝混用。如果你用了 MyBatis-Plus,TableField配合全局DatePattern.NORM_DATETIME_PATTERN也能做到。5.3 金额既传了6,800又传了6800,前端合计出错现象:订单列表接口,金额字段有时候返回0.00,有时候返回0,有时候返回1,000.00;订单详情接口返回的是1000,前端累加时parseFloat(1,000.00)得到NaN。原因:后端没有区分「展示字段」和「计算字段」,同一个字段按不同场景返回了不同格式,前端拿到什么用什么,一算就炸。解决:展示接口按文档返回格式化字符串,计算接口返回数字,字段名分开。我在老项目中引入过一个约定:凡是在字段名后面加_display的,一律是格式化后的展示值;没有后缀的,一律是原始数字,前端可以做运算。电话号码、邮箱这类也一样,*号脱敏字段用masked_前缀。5.4 GET 请求在网关被缓存,改了数据前端看不到现象:一个管理后台的“上下架”接口用的 GET 方法,前端调用后提示成功,但列表数据没有变化——查了半天发现是 CDN 或网关缓存了 GET 请求的响应,前端一直拿到的是旧数据。原因:文档提供的 GET 版更新/删除接口,默认语义和 POST 不同,一旦经过带缓存的代理层,响应会被无条件缓存。解决:生产环境强制弃用 GET 做写操作,只保留 POST 的 add/update 版本;或者网关层对/update、/delete_by_id这类路径强制设置Cache-Control: no-store。我在一个跨平台项目里,直接在网关规则上加了「所有含/admin/前缀的 GET 请求默认不缓存」这条策略,才把这个问题根除掉。5.5 requestId 没有透传,线上问题查不到日志现象:某平台反馈“下单失败”,后端查日志时只能看到一堆 no auth 的报错,却定位不到是哪一次请求、哪条链路出了问题。原因:文档规定了接口需携带requestId唯一标识追踪问题,但实际代码里只把 requestId 放进响应体,没有在日志上下文里透传,导致排查时无法关联上下游调用。解决:在后端网关或过滤器里统一生成requestId,塞进日志的 MDC 上下文,再透传给下游服务,同时响应体里也带上同一个值。前端在调接口出错时,把响应里的requestId带上工单,后端按这个 ID 一搜日志就能定位全链路。我现在的做法是:requestId UUID.randomUUID().toString().replace(-, ),用首尾 8 位做日志索引,足够快又不失唯一性。6. 把这份规范变成团队模板一份可提交评审的接口自查清单文档本身是静态的,落地才是动态的。我一般会把这份《API接口设计规范.docx》转成一份接口开发自查清单,在每次接口提测前强制对照检查一遍。下面是最常用的 12 条检查项,可以直接复制进你的项目 Wiki:# 接口开发自查清单 ## 路径与版本 - [ ] path 以 /api/v1/ 开头,版本号统一 - [ ] URL 中不出现动词缩写,行为遵循 add/update/get_by_id/list_by_param - [ ] 写操作统一 POST,不裸露 GET 写接口 ## 参数 - [ ] 分页参数默认 page1per_page20,排序字段走白名单 - [ ] 金额字段区分展示值(格式化)与计算值(数字) - [ ] 时间统一 yyyy-MM-dd HH:mm:ss,不含时区后缀 ## 响应与安全 - [ ] 响应体只走 ResultT 壳子,data 不为 null - [ ] 空列表返回 [] ,空字符串返回 ,数字默认 0 - [ ] 隐私字段已按规则脱敏,不返回原始手机号/身份证 - [ ] 敏感接口参数已做签名校验 ## 追踪 - [ ] 响应头或响应体携带 requestId,日志中可检索 - [ ] 接口文档字段与返回 JSON 字段一致,无遗漏无歧义我还习惯在清单末尾补一行“接口评审人签字/日期”,因为规范写得再好,没有评审动作就永远只是一份 docx。我踩过的最大的坑就是把规范文档直接扔给团队,结果三个月后回头看,接口风格还是百花齐放——因为没有任何人在提测前把这 12 项逐条核对过。从那以后我每个接口提测都强制走一遍这份自查清单,再拉后端和前端负责人一起过一眼,接口风格的统一度才真正稳定下来,希望这份清单对你也有同样的效果。最后分享一个使用心得:这份文档里最有价值的不是 RESTful 设计原则,而是那几条“前端不干活”的约定——JSON 合法性归后端、空值统一由后端兜底、展示格式由后端把控。这三条的执行,决定了你后期能省多少联调时间。文档拿过去之后,建议先把它转成自己团队接口模板的ResponseBody父类,再转成自查清单,两步走完,规范才算是真正落地了。本文还有配套的精品资源点击获取