前后端分离下的API接口设计规范:从JSON格式到错误码统一落地
简介《API接口设计规范》文档是一份面向后端开发与前后端协作团队的标准参考旨在统一API接口的开发、实现与维护约定解决接口风格不一、参数混乱、错误处理分散等常见问题。文档在数据格式、路径版本、请求/响应参数等层面给出细化规则例如key采用驼峰命名、金额以元为单位且显示格式化、空对象/列表用[]、避免null与undefined以及每次接口携带requestId用于追踪问题路径统一为/{版本}/{业务功能}/{表名}/{行为}请求参数支持limit、offset、page、per_page、sort_by、order响应统一为code、success、message、data结构并明确200、400、401、403等错误码语义。安全性方面则覆盖token身份验证、权限控制与HTTPS数据加密。资源包共1个docx文件仅20KB内容精炼、便于直接查阅和团队分享已有3578人学习。对于需要快速建立接口规范或提升接口质量的团队这份文档可作为落地模板帮助减少前后端联调成本并提高可维护性。1. 需求背景接口格式不统一联调就像开盲盒前后端联调最怕的不是功能做不完而是接口格式各写各的。上个月做的一个项目后端把空列表返回成 null前端一渲染直接白屏两边对着同一个接口查了一下午最后发现是接口文档里根本没约定空值怎么返回。这份 api 接口设计规范要解决的就是这类问题把 JSON 格式、金额单位、时间格式、路径结构、错误码全部提前写成硬性约定前后端照着同一套规则开发联调时不用再互相猜。它适合正在做前后端分离项目的开发团队也适合需要提交可落地接口设计方案的毕设和课设项目——拿它当模板照着改就能用。2. 全局数据格式约定JSON、金额、时间与空值的统一口径一份接口规范最先要立住的是数据格式。格式不统一后面所有接口设计都是空中楼阁。这一章讲清楚后端必须保证的 JSON 合法性、金额单位、时间格式、空值规则和 key 命名习惯这些约定属于全局基线任何一个接口都要遵守。2.1 JSON 合法性与 Content-Type合法性是后端的责任约定一后端需要保证 JSON 格式的合法性前端不对格式的合法性做判断。这句话看起来很绝对实际上是划清责任边界。接口返回的 JSON 是由后端序列化产生的后端拥有完整控制权序列化层出了问题应该后端修复而不是让每个前端调用方各自做容错解析。响应头 Content-Type 固定为application/json; charsetUTF-8。这里 charsetUTF-8 不能丢否则中文可能出现乱码也不要顺手改成 text/plain 或 application/x-www-form-urlencoded前端的 axios 和 fetch 默认解析逻辑依赖这个头。{ code: 200, success: true, message: 操作成功, data: { list: [], total: 0 }, timestamp: 1742000000000 }这个响应体里 code 是数字状态码success 表示业务是否成功message 是给人看的提示文案data 里放业务数据timestamp 是服务端返回时间。前端拦截器通常会先判断 success 再进业务逻辑所以这两个字段必须稳定不能因为某个接口特殊就改名。如果后端返回的是非法 JSON前端解析直接抛错这类问题必须在后端日志里暴露而不是让用户看到白屏。2.2 金额、时间与数字单位统一展示和计算分离金额字段是接口里最容易翻车的。规范约定所有金额以元为单位显示性的后台返回的是格式化之后的字符串例如6,800。也就是说展示层字段如 formattedAmount 是带千分位的字符串计算层字段如 amount 是纯数字二者分工明确。如果前后端各用一套单位订单总额小数位对不上排查起来非常痛苦。时间格式统一为字符串2019-01-01 12:12:12。注意这里是字符串不是时间戳。虽然时间戳传输上更轻量但开发时打开响应看到 1742000000000 完全不知道是几点而字符串一眼能看懂省去在线转换工具。涉及时区时建议在字段注释里写明是服务器本地时间还是 UTC后端实体类上用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)保证输出一致。默认数字使用 0不要返回 null。比如用户积分字段数据库没有值就返回 0前端可以直接做数值运算不需要再判空。常见的误用是在 SQL 里用SUM()聚合结果为空时返回 null这个问题可以在 mapper 层用COALESCE(SUM(amount), 0)解决。2.3 空值策略空对象、空列表、空字符串各归其位这条规范最反直觉的地方在空值处理空对象请使用[]空列表也使用[]空字符串使用默认数字使用0尽量避免使用 null 和 undefined。严格来说 JSON 的空对象是{}但这份规范把空对象和空列表统一收敛到[]目的是让前端判断逻辑足够简单——看到数组且 length 0 就知道是空不用再区分类型。我一般会建议把这条当代码评审的硬性检查项所有接口返回的字段要么有值要么是[]、、0不允许出现 null。理由很实在前端模板引擎遇到 null 会抛异常或显示空白而[]和天然安全。如果某个字段确实是对象类型且为空可以在字段注释里约定返回{}但必须写清楚不能让人猜。// 空列表兜底防止序列化后输出 null if (CollectionUtils.isEmpty(result.getList())) { result.setList(Collections.emptyList()); }这段兜底逻辑放在 service 层谁调谁都安全。把空值策略定死之后前端所有组件可以默认响应里没有 null代码里彻底少掉一半判空逻辑。2.4 key 命名、requestId 与敏感字段脱敏key 统一使用驼峰命名首字母小写例如userId、orderList、displayName。严禁出现user_id或UserId否则每个接口前端都要写一层映射。数据接口中定义的 key 集合是后端返回的子集意思是后端可以返回更多字段但规范中承诺的 key 一个都不能缺——这是为了防止前端拿到一个缺字段的对象后渲染异常。每个接口应该携带 requestId作为一次请求的唯一标识用来追踪问题。用户反馈接口异常时前端把 requestId 贴给后端后端在日志里 grep 到这个 ID 就能定位整条调用链路不用再问你是几点几分操作的。requestId 可以由网关生成也可以在后端入口过滤器里用 UUID 生成后放入响应头或响应体。涉及用户隐私的字段需要加*号脱敏比如手机号返回138****8000身份证号返回310***********1234邮箱、支付账号、邮寄地址同样处理。脱敏规则最好统一不要每个接口各自实现否则有的接口脱敏、有的不脱敏等于没脱敏。通常的做法是写一个序列化注解统一处理业务代码里完全不用关心脱敏逻辑同时对敏感度高的接口参数做签名匹配防止参数在请求过程中被抓取篡改。注意脱敏和签名是两件事。脱敏解决的是展示层隐私泄露签名解决的是传输层参数篡改二者都要做不能互相替代。3. 路径与行为设计从 /{版本}/{业务}/{表名}/{行为} 看接口的语义化拆分路径设计决定了接口好不好找、好不好维护。这份规范把路径模板定成path /{版本}/{具体的业务功能}/{表名}/{行为}下面逐段拆开讲并给出具体示例和路由解析顺序。3.1 路径模板四段式结构与缺省行为模板完整拼出来是/{版本}/{业务功能}/{表名}/{行为}。版本在最前面是为了未来升级不破坏老客户端业务功能用来划分模块表名对应数据库物理表行为表示对资源做什么操作。看几个原文里的例子/api/v1/acs/users/表示访问所有的用户资源。/api/v1/acs/users/1表示访问 Id 为 1 的用户。/api/v1/acs/users/group/iws-tech/minAge/30表示访问 iws-tech 组中最小年龄 30 岁的用户。/api/v1/acs/accounts/UpdateAllUserFlags表示更新所有用户的某个标识。注意第二个例子路径停在/users/1没有显式行为名。这里其实省略了默认行为 getById属于资源型接口的变体并不是路径写错了。我的处理建议是如果路径以资源 ID 结尾默认行为是 get如果后面跟的是行为名则执行对应方法如果带了筛选条件如group/iws-tech/minAge/30默认走 list。路由解析优先级为行为名 资源 ID 筛选条件。这样设计的好处是可以直接从 URL 猜出接口语义。看UpdateAllUserFlags就知道是批量更新操作看结尾是数字就知道在访问某个具体资源不需要翻文档确认。筛选型参数可以继续追加例如/api/v1/acs/users/group/iws-tech/minAge/30/status/1每对参数表示一个维度解析逻辑保持一致。3.2 版本号与业务功能命名数据库前缀剥掉后的语义收敛版本号开始时全部为 V1。我一般建议格式是 V 加大写数字升级大版本时变成 V2小版本升级不改变路径在响应体的版本字段标记即可不必每次改 URL 导致老客户端失效。业务功能这一段值得细看。原文说App 的 setting数据库命名为app_setting那么具体的业务功能为 setting架构组的 wechat数据库命名为arch_wechat那么具体的业务功能为 wechat。这里藏着一条潜规则数据库表前缀app_、arch_是物理归属标识而暴露给前端的业务功能名把前缀剥掉只保留语义部分。这么做有两个好处。第一前端不需要知道app_、arch_这种内部实现细节业务功能和数据库表结构解耦第二一个业务功能下可以挂多张表比如 setting 业务功能下可以有app_setting、setting_log、setting_template多张表URL 层面仍然统一收纳在/setting/下后续做权限控制也只需要对业务功能粒度的路径做规则配置。3.3 HTTP 方法与行为映射一张表把七个动作定死规范给出了七个核心行为每个行为绑定一个 HTTP 方法和固定路径后缀避免同一个操作在不同接口里叫法不同HTTP 方法行为方法名路径后缀说明POSTadd(POJO)/add添加对象请求体接收 JSONGETinsert(param...)/insert?param?与 add 作用相同参数走 queryGETdeleteById(long)/delete_by_id?id?逻辑删除数据库不物理删行POSTupdate(POJO)/update整体更新对象接收 JSONGETupdateById(long)/update_by_id?id?按 ID 更新GETgetById(long)/get_by_id?id?按 ID 查询GETlistByParam(Object)/list_by_param?param?page2列表查询支持分页排序原文的list_by_param拼写作list_bu_param规范里应统一为list_by_param与驼峰命名对齐。还有两点值得说明deleteById 是逻辑删除数据库里不删除数据但业务上有删除语义这是为了历史数据可追溯update 和 add 都要求接收 JSON适合前端提交复杂对象insert 和 updateById 走 query 参数适合调试时直接在浏览器里验证。设计这套映射时我一般遵循一个原则动作尽量少语义尽量显式。如果团队里每个人都自己发明动词比如 remove、del、modify前端对接成本会成倍上升。七个动作覆盖了绝大多数 CRUD 场景遇到批量操作补一个/batch后缀即可。为什么不严格用 REST 风格的 PUT/DELETE很务实的原因部分网关和代理环境对 PUT/DELETE 支持不友好GET 和 POST 最稳定而且团队成员读方法名就能理解行为学习成本更低。4. 请求参数与响应封装从分页参数到 ResultT 的实现细节接口参数和响应体是前后端直接接触的部分设计得好联调效率高设计不好就变成黑匣子。这一章把通用查询参数、统一响应结构和 Java 错误码枚举的落地方式完整过一遍。4.1 通用查询参数limit、offset、page、per_page 怎么选规范里给出了两组翻页方式limit offset 和 page per_page再加上排序参数 sort_by order以及筛选条件 product_type原文写作 producy_type应为笔误规范中按正确拼写处理。limit10指定返回记录的数量。offset10指定返回记录的开始位置。page2per_page100指定第几页以及每页的记录数。sort_bynameorderasc按 name 属性升序排序。product_type1指定筛选条件。具体怎么选offset/limit 适合数据量小、深度翻页要求不高的场景SQL 直接LIMIT 10 OFFSET 10page/per_page 适合后台管理列表页面上有页码选择器后端计算 offset 的方式是(page - 1) * per_page。深度翻页超过一万条时这两种方式都会变慢通常的做法是换游标方式但规范里仍保留 offset 作为兜底。排序参数必须白名单化后端不能直接把用户传来的字段名拼进 ORDER BY否则存在 SQL 注入风险。比如允许排序的字段只有name、created_at、amount后端收到 sort_by 后先查白名单不在名单里直接忽略排序返回默认顺序。响应数据总条数统一放在data.total里前端分页组件需要用它计算总页数。4.2 统一响应体ResultT 的结构与前端拦截器规范贴出的响应体 JSON 结构如下code 是数字success 是布尔message 是提示data 里放 list 数组和 total 总数timestamp 是服务端时间戳。这套结构解决了一个常见问题每个接口各自定义{status: 1}、{ret: ok}前端根本无法统一处理错误。统一之后前端 axios 拦截器可以收敛成一份代码// axios 响应拦截器 axios.interceptors.response.use( (response) { const res response.data if (res.code 200 res.success) { return res.data } // 非 200 统一走错误提示 alert(res.message) return Promise.reject(new Error(res.message)) } )这段拦截器的逻辑很简单HTTP 层成功后再看业务层的 code 和 success只要业务失败就统一提示并 reject。前端业务代码里拿到的已经是res.data不用再关心外层包装字段错误处理也收敛到一个地方。这里有一个容易踩的坑HTTP 状态码和业务 code 是两回事。200 表示 HTTP 层请求成功但业务可能失败所以必须再看 success 字段。比如登录过期HTTP 返回 200但业务 code 是 401拦截器里需要单独处理跳转登录页而不是直接 alert。4.3 错误码枚举与 ResultT 的 Java 落地规范给出了 ResultCode 枚举和 Result 泛型类的源码。先看枚举public enum ResultCode { SC_OK_200(200, 操作成功), SC_MISSING_ARGUMENT_400(400, 请求格式或参数不对), SC_UNAUTHORIZED_401(401, 未授权), SC_VALID_EXCEPTION(403, 请求参数校验失败), SC_NOT_FOUND_404(404, 请求的地址未找到), SC_METHOD_NOT_ALLOWED_405(405, 不支持的 HTTP 方法), SC_INTERNAL_SERVER_ERROR_500(500, 内部程序错误); private final Integer code; private final String msg; ResultCode(Integer code, String msg) { this.code code; this.msg msg; } public Integer getCode() { return code; } public String getMsg() { return msg; } }这个枚举做的事情是把状态码和提示语绑定避免每个接口里散落魔法数字。注意 403 在这里的语义是参数验证错误而不是权限不足和 Spring Security 的 403 概念不一样写代码时要在注释里特别标明免得后端同事误用。再看 Result 类的静态工厂方法。业务代码里只需要这样用// 查询用户列表 public ResultObject userList() { ListUser list userService.list(); return Result.ok(list); } // 参数校验失败 public ResultObject badParam() { return Result.validException(用户ID不能为空); }这段代码把组装响应结构这件事从业务方法里彻底解放出来业务代码只关心返回什么数据和抛出什么错误统一包装交给 Result 类。需要补充的是实际项目里异常不是每个方法手动抛而是写一个全局异常处理器捕获参数校验异常、业务异常和未知异常分别映射到 400、403、500这样整个项目对错误码的使用才是可控的。5. 落地避坑这套接口规范最容易翻车的五个细节规范写出来容易落下去难。下面五个问题是我在项目里真实遇到过的按「现象 → 原因 → 解决」来写覆盖空值、金额、时间、路径和隐私五个高频翻车点。5.1 空对象返回 null前端渲染直接白屏现象接口没有数据时data.list返回了 null前端模板里一遍历就报错页面直接白屏。测试环境有数据所以发现不了生产环境某用户没有订单记录就触发了。原因后端在 service 层写了return null序列化后变成list: null而规范要求空列表必须返回[]。问题根源是规范没有被强制执行空值兜底只存在于文档里。解决在 service 层强制空列表返回Collections.emptyList()或者在统一响应包装层做兜底发现 list 为 null 就替换成空数组。这样无论哪个业务方法漏了最终响应都不会出现 null。前端也约定好只要接口按规范返回view 层不再写判空逻辑。5.2 金额单位没统一账单金额差一百倍现象订单详情接口返回6800积分接口返回68.00前端展示时有的当元、有的除以 100金额全对不上上线第一天就收到客诉。原因有人习惯用「分」存金额有人用「元」存接口文档没有在字段注释里写死单位。这类问题隐蔽性很强因为单看数字看不出问题只有跨模块联调时才会暴露。解决按规范把金额单位统一为元。展示字段用字符串且带千分位如6,800计算字段用数字类型单位为元。所有金额字段在接口文档里必须带单位注释代码评审时凡是没写单位注释的金额字段直接打回。数据库存分的表在出接口时换算换算逻辑集中在一个工具类里。5.3 时间格式各传各的排查问题全靠猜现象一部分接口返回2019-01-01 12:12:12另一部分返回1546301532前端为了兼容写了两个格式化函数后端排查线上问题时还得先确认这是字符串还是时间戳。原因没有在规范层面规定时间格式数据库里 datetime 类型序列化后是字符串而某些接口直接返回了 long 时间戳两边没有对齐。解决统一用字符串yyyy-MM-dd HH:mm:ss。后端实体类的日期字段上加序列化注解强制格式前端封装一个全局的日期解析函数接口文档里所有时间字段统一标注为字符串。如果涉及时区在字段注释里写清楚 GMT8不要留给调用方猜。5.4 路径语义混乱接口一多就失控现象项目里有/getAllUser、/api/user_list、/users/queryAll三种风格共存新同学根本不知道去哪找接口同一个用户的查询逻辑散落在三个文件里。原因没有路径模板约束每个人按自己习惯命名有人用驼峰方法名有人用下划线还有人直接写动词开头。接口数量一多整个模块变成无法维护的黑匣子。解决强制按/{版本}/{业务功能}/{表名}/{行为}设计路径例如/api/v1/acs/users/list_by_param?page1per_page10。发现不符合规范的路径在评审阶段打回不要等上线后统一重构。这个检查可以用脚本完成扫描后端所有接口注解里的路径正则校验是否匹配模板不匹配的输出警告建议放进 CI 每次提交自动跑一遍。5.5 敏感字段明文返回隐私数据裸奔现象手机号、身份证号原样返回被第三方抓包后直接泄露安全测试把这个问题标记为高危漏洞。排查发现脱敏规则每个接口各写各的有的接口压根没写。原因没有在接口层做统一脱敏数据库存什么就返回什么只有个别前端发现需要脱敏时才在响应里处理属于典型的控制面失控。解决按规范对手机号、身份证、邮箱、支付账号、邮寄地址统一加星号。通常的做法是写一个序列化注解在字段上标注后自动脱敏业务代码完全不用感知。同时对敏感度高的接口参数做签名匹配客户端和服务器通过约定算法对参数值签名防止参数在请求过程中被抓取篡改。签名校验不过的请求直接返回 400不进入业务逻辑。注意脱敏是展示层兜底签名是传输层兜底。两者配合使用才能在日志、数据库、网络链路三个环节都控制住隐私数据扩散。6. 验收与答辩怎么证明这套规范真的落地了写规范的最终目的是被遵守。验收时我一般不做通读检查而是建一份可勾选的清单每一条对应一个可验证的动作挨个过一遍更高效。6.1 一份可执行的接口验收清单检查项验证动作达标标准JSON 合法性调用所有接口用 JSON 解析器解析响应体全部能按 application/json 解析空值策略构造空数据场景空列表、空字符串响应里不出现 null/undefined金额单位抽查金额字段并对比数据库单位统一为元展示字段带千分位路径模板扫描全部接口路径全部匹配 /{版本}/{业务}/{表名}/{行为}错误码触发 400、401、403、404、500code 与提示语与规范一致隐私脱敏抓包检查手机号、身份证字段均按规则打星号更快的做法是写一段小脚本把后端所有接口注解里的路径扫出来正则校验路径模板是否合规不合规的打印警告。这个脚本建议放进 CI每次提交都跑一遍把规范从纸面变成机器检查。接口数量多的时候人工通读规范是最低效的验收方式机器扫描十秒就能完成。6.2 答辩时怎么讲这套设计如果你在毕设或课设答辩中用到这套规范记住三个讲法先讲痛点接口格式不统一导致联调成本高再讲方案四段式路径、统一响应体、错误码枚举、脱敏签名最后拿一个真实接口完整走一遍从请求到响应把规范点串起来比空讲设计原则有说服力得多。答辩老师最常问的问题是「为什么用 GET 做删除而不是 DELETE」。回答思路是这套规范优先保证网关兼容性和团队可读性逻辑删除本身不需要真正 DELETE 语义GET 可以直接在浏览器验证如果业务场景确实需要严格 RESTful 风格可以留一个适配层做转换不影响前端对接。从那以后我每接一个新的前后端分离项目第一件事不是写业务而是先把接口规范整体读一遍强制自己跑通一个最小闭环再动手写功能。很多联调事故其实在按规范跑通最小闭环那一刻就已经躲开了。希望帮到你。本文还有配套的精品资源点击获取