简介:本资源是一份面向后端开发工程师、API设计人员及技术团队的《API接口设计规范》标准文档,聚焦RESTful风格接口的全生命周期管理,解决多团队协作中接口不一致、维护成本高、安全风险大等实际问题。文档涵盖设计原则、路径规划(含版本控制与业务路由)、请求/响应参数约定、JSON数据格式(驼峰命名、空值处理、时间金额格式)、错误码体系(200/400/401/403/404/405/500全场景说明)及安全性实践(Token鉴权、敏感字段脱敏、参数签名防篡改),并附有典型HTTP方法映射示例与Java Result 统一响应结构定义。资源为单个20KB的Word文档(.docx),内容完整、即开即用,已供3579人学习下载,可直接作为团队内部API开发基准或新人培训材料,显著提升接口质量、降低联调返工率与线上故障排查成本。
1. 这份 API 接口设计规范不是“参考文档”,而是你上线前必须过一遍的「接口宪法」:它不讲理论,只定义后端返回什么、前端怎么信、联调时谁背锅
你有没有遇到过这样的场景:前端在控制台里看到data: null,后端坚称“我返回了空数组”,结果抓包发现后端实际返回的是null;或者测试提单说“手机号没脱敏”,你翻代码发现字段名是mobileNo,但规范里写的是mobile,而脱敏逻辑只匹配了后者;又或者压测时突然大量 400,排查半天发现是某接口把page=0当合法参数接收了,但规范明文规定page从 1 开始。这份《API接口设计规范.docx》不是那种写完就锁进共享盘吃灰的模板文件——它是用血泪经验凝练出的接口契约白名单:所有 key 名、空值形态、时间格式、分页语义、错误码映射、甚至 requestId 的生成时机,都强制对齐。它面向的是真实交付现场:后端开发自测时要按它校验 JSON 结构,前端封装 request 工具时要按它解析 data 字段,测试同学写用例时要按它断言响应体字段类型,运维查日志时要按它过滤requestId。它不解决“要不要微服务”,但能让你少改三版联调接口、少接五个半夜告警电话、少写十份“为什么这个字段是 null”的说明邮件。如果你正在启动一个新模块、接手一个老系统、或刚被指派为跨团队接口协调人,这份规范就是你技术决策的锚点——不是“建议这么做”,而是“必须这么交”。
2. 数据格式与空值契约:JSON 不是自由发挥的画布,而是带钢印的模具
2.1 为什么空对象必须是[]而不是null?——从 V8 引擎到 Axios 拦截器的链路真相
规范里那句“空对象请使用[]”看似简单,实则直击前端运行时最脆弱的一环。我们来看真实链路:当后端返回{ users: null },前端用response.data.users.map(...)会直接抛TypeError: Cannot read property 'map' of null;而若返回{ users: [] },map安然执行(返回空数组)。更隐蔽的是 Axios 默认配置:transformResponse会将null原样透传,但某些自定义拦截器(如做统一 loading 状态管理)可能对data做Object.keys(data),而Object.keys(null)抛错。规范强制[]是在切断错误传播链。验证方法很简单:用 curl 模拟请求,检查响应体中所有应为集合的字段是否为[]。
curl -X GET "http://localhost:8080/api/v1/acs/users/list_by_param?per_page=5" \ -H "Content-Type: application/json; charset=UTF-8" # 检查返回中 users 字段:✅ {"users": []} ❌ {"users": null}提示:后端 Spring Boot 项目需在全局序列化配置中禁用
WRITE_NULLS,并在 DTO 字段上加@JsonInclude(JsonInclude.Include.NON_EMPTY),而非依赖@NotNull注解——后者只校验入参,不约束出参。
2.2 金额与时间的“格式化陷阱”:为什么后端要返回"6,800"而不是6800.00?
规范要求“金额以元为单位,显示性返回格式化字符串”,这反直觉但极其关键。新手常犯的错是后端返回数字6800.00,前端用Intl.NumberFormat格式化——问题在于:不同地区千分位符号不同(,vs.),且小数位数需动态适配(人民币无小数,日元需两位)。当后端统一返回"6,800",前端只需原样渲染,彻底规避国际化格式混乱。同理,时间字段"2019-01-01 12:12:12"是 ISO 8601 的子集,所有主流语言(JavaScript 的new Date()、Python 的datetime.strptime)都能无歧义解析,比时间戳1546344732000更易调试。验证时重点看三个字段:amount、create_time、update_time,用正则/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/匹配时间,用/^\d{1,3}(,\d{3})*(\.\d+)?$/匹配金额字符串。
2.3 驼峰命名与requestId的双重保险:如何让一次请求从 Nginx 日志贯穿到业务 DB
key必须驼峰(userMobile)、requestId必须存在,这两条是分布式追踪的生命线。假设用户投诉“订单状态没更新”,你有requestId: req_abc123,就能在 ELK 中串联:Nginx access log → Spring Cloud Gateway 日志 → 订单服务 traceId → MySQL 慢查询日志。但如果后端返回user_mobile,前端存入 Redux 的 key 就是user_mobile,而监控系统埋点脚本却按userMobile取值,导致追踪断裂。requestId的生成必须在网关层(如 Spring Cloud Gateway 的GlobalFilter),而非业务服务内,否则重试时会生成新 ID。验证命令:
curl -v "http://localhost:8080/api/v1/acs/users/1" 2>&1 | grep -E "(requestId|X-Request-ID)" # ✅ 响应头含 X-Request-ID: req_abc123,响应体 data 中含 "requestId": "req_abc123" # ❌ 仅响应头有,或响应体无此字段3. 路径设计与 HTTP 方法映射:RESTful 不是玄学,是可穷举的路由状态机
3.1 路径结构/api/v1/{business}/{table}/{action}的四个不可妥协维度
规范定义的路径模板不是为了“看起来像 REST”,而是为自动化工具链提供确定性输入。/api/v1/acs/users/中:
v1:版本号必须为v{数字}格式(禁止v1.0或beta),便于 Nginx 用正则^/api/v(\d+)/统一转发;acs:业务功能名必须与数据库 schema 名一致(如acs对应acs_user表),避免前端路由与后端表名认知割裂;users:表名必须为复数、小写、下划线分隔(users而非user或Users),这是 Swagger 自动生成文档的基础;list_by_param:行为名必须用下划线分隔的动词短语,与 Java 方法名listByParam严格对应,确保@ApiOperation注解能精准绑定。
验证时用curl -I检查路径是否符合正则/^\/api\/v\d+\/[a-z_]+\/[a-z_]+\/[a-z_]+$/,并确认该路径在 Spring Boot 的@RequestMapping中被@RestController显式声明。
3.2 HTTP 方法与行为的硬性绑定:为什么POST /update是红线,而POST /update_by_id是合规的
规范表格中明确区分了POST /update(接收 POJO)和GET /update_by_id?id=1(接收 ID 参数),这背后是幂等性与缓存策略的物理约束。GET请求可被浏览器、CDN、代理服务器缓存,若用GET /update修改数据,用户刷新页面会重复提交;而POST /update因携带 JSON body,天然不被缓存。但POST /update的风险在于:前端若未正确设置Content-Type: application/json,后端 Spring MVC 的@RequestBody会绑定失败,返回 400。因此规范强制POST /update必须配合@Valid校验,而GET /update_by_id则用@RequestParam+@Min(1)校验。验证要点:用 Postman 发送GET /update_by_id?id=abc,应返回 400(ValidException),而非 500。
3.3 分页参数的语义战争:page/per_page与limit/offset的共存逻辑
规范同时允许page=2&per_page=10和limit=10&offset=10,这不是妥协,而是兼容历史系统与新架构的务实设计。page/per_page符合人类直觉(第2页,每页10条),适合管理后台;limit/offset是数据库物理偏移,适合大数据量导出场景(offset可达百万级)。但二者不能混用!规范要求:若请求中同时存在page和limit,后端必须拒绝(400)。验证脚本:
import requests # 测试混合参数 resp = requests.get("http://localhost:8080/api/v1/acs/users/list_by_param?page=1&per_page=10&limit=5") assert resp.status_code == 400, f"混合分页参数未被拒绝: {resp.status_code}" # 测试 page/per_page 正确性 resp = requests.get("http://localhost:8080/api/v1/acs/users/list_by_param?page=1&per_page=10") data = resp.json() assert "total" in data["data"], "响应中缺失 total 字段" assert len(data["data"]["list"]) <= 10, "返回列表长度超过 per_page"4. 响应结构与错误码体系:为什么Result<T>类不是装饰,而是防御性编程的盾牌
4.1Result<T>的泛型设计:如何让 TypeScript 前端获得 100% 类型安全
Java 的Result<T>类通过@Data和泛型T,使 Lombok 自动生成getData(): T方法。这直接支撑前端 TypeScript 的类型推导:当后端定义Result<UserVO>,Swagger 生成的 TS 接口即为Promise<Result<UserVO>>,response.data类型自动为UserVO,而非any。关键在@ApiModel和@ApiModelProperty注解——它们是 Swagger 解析泛型的唯一依据。若遗漏@ApiModel,Swagger 会将data解析为Object,导致前端类型丢失。验证方法:启动 Swagger UI,查看/api/v1/acs/users/get_by_id的响应模型,确认data字段类型为UserVO(而非object)。
4.2 错误码枚举ResultCodeEnum的物理落地:403 不是“参数错”,而是“校验失败”的精确打击
规范将403明确定义为ValidException(参数验证错误),这区别于400 MissingArgument(参数缺失)和401 Unauthorized(认证失败)。物理意义在于:403触发的是 JSR-303@Valid校验失败,如@Email注解不通过;而400是 Spring MVC 无法绑定参数(如id=abc传给Long id)。这种区分让前端能精准提示:“邮箱格式不正确”(403)vs “请填写邮箱”(400)。验证时故意传非法邮箱:
curl -X POST "http://localhost:8080/api/v1/acs/users/add" \ -H "Content-Type: application/json" \ -d '{"email":"invalid-email"}' # ✅ 返回 403,code=403,message="请求参数校验失败" # ❌ 返回 400 或 5004.3timestamp字段的时区陷阱:为什么必须用System.currentTimeMillis()而非new Date()
Result类中timestamp直接赋值System.currentTimeMillis(),这是为规避 JVM 时区配置差异。若用new Date().getTime(),当服务器时区设为Asia/Shanghai,而 Docker 容器未挂载时区文件,new Date()可能返回错误时间戳。System.currentTimeMillis()返回自 Unix epoch 的毫秒数,与时区无关,前端new Date(timestamp)可正确转换为本地时间。验证:在不同时区的机器上运行单元测试,检查Result.ok().getTimestamp()是否与System.currentTimeMillis()差值 < 10ms。
5. 安全与脱敏实践:签名验证与敏感字段的“双锁机制”
5.1 参数签名算法的最小可行实现:HMAC-SHA256 的三要素校验
规范要求“客户端和服务器通过约定算法对参数值签名”,其核心是HMAC-SHA256 + 时间戳 + 随机串。最小可行方案:客户端将所有请求参数(除sign外)按字典序拼接为key1=value1&key2=value2,再拼接&t=1712345678&nonce=abc123,最后用密钥HMAC-SHA256计算摘要。后端收到后:
- 校验
t是否在 5 分钟有效期内(防重放); - 校验
nonce是否已存在于 Redis(去重); - 用相同逻辑重新计算签名,比对
sign字段。
关键点:密钥绝不传输,nonce必须单次使用。验证脚本需模拟客户端签名逻辑,与后端比对。
5.2 敏感字段脱敏的“正则白名单”:为什么mobile必须脱敏而userName不需要
规范列出“手机号、身份证、邮箱、支付账号、邮寄地址”必须脱敏,其依据是《个人信息安全规范》GB/T 35273-2020 的“去标识化”要求。技术实现上,后端在Result<T>序列化前,用正则替换:
- 手机号:
(\d{3})\d{4}(\d{4})→$1****$2 - 身份证:
(\d{4})\d{10}(\d{4})→$1**********$2 - 邮箱:
(^.+)@(.+\.)→$1***@$2
注意:脱敏必须在@JsonSerialize序列化阶段完成,而非 SQL 查询时——否则SELECT * FROM users会泄露原始数据。验证:用curl获取用户详情,检查mobile字段是否为138****1234格式。
5.3 HTTPS 与Content-Type的强制组合:为什么application/json; charset=UTF-8缺一不可
规范强制Content-Type: application/json; charset=UTF-8,这是为解决两个经典问题:
- 若仅写
application/json,某些旧版 IE 会触发下载对话框; - 若缺
charset=UTF-8,中文字段(如message: "操作成功")在部分 Nginx 配置下会乱码。
物理验证:用curl -I查看响应头,必须包含Content-Type: application/json; charset=UTF-8。若缺失,需在 Spring Boot 的application.yml中配置:
spring: http: encoding: charset: UTF-8 force: true web: resources: add-mappings: false6. 避坑指南:那些让联调崩溃的 5 个边界问题与血泪解决方案
6.1 现象:前端收到{"code":200,"success":true,"data":null},但后端日志显示data是空集合
原因:后端在Result<T>构造时,对空集合做了if (list.isEmpty()) data = null的手动赋值,违反了规范“空列表请使用[]”。Spring Boot 的@ResponseBody默认序列化null为null,而非[]。
解决:删除所有手动data = null逻辑,在Result类的setData(T data)方法中增加判空处理:
public void setData(T data) { if (data instanceof Collection && ((Collection<?>) data).isEmpty()) { this.data = (T) new ArrayList<>(); // 强制转为空列表 } else { this.data = data; } }6.2 现象:/api/v1/acs/users/1返回 404,但GET /users/1在 Postman 中能通
原因:Spring Boot 的@RequestMapping("/api/v1/acs")与 Controller 的@RequestMapping("/users")拼接后为/api/v1/acs/users,但前端请求路径多写了斜杠(/api/v1/acs/users//1),而 Spring MVC 默认不忽略重复斜杠。
解决:在application.yml中启用路径优化:
server: servlet: context-path: / tomcat: redirect-context-root: false spring: mvc: static-path-pattern: /static/** # 启用路径规范化 pathmatch: matching-strategy: ant_path_matcher6.3 现象:sort_by=name&order=desc导致 SQL 注入,数据库报错Unknown column 'name desc'
原因:后端直接将sort_by参数拼接到 SQLORDER BY子句,未做白名单校验。攻击者传sort_by=name,(SELECT password FROM users WHERE id=1)即可注入。
解决:建立排序字段白名单,sort_by只允许name,create_time,status等预定义字段:
private static final Set<String> SORT_FIELDS = Set.of("name", "create_time", "status"); if (!SORT_FIELDS.contains(sortBy)) { throw new ValidException("非法排序字段: " + sortBy); } // 安全拼接 String sql = "ORDER BY " + sortBy + " " + (Objects.equals(order, "desc") ? "DESC" : "ASC");6.4 现象:requestId在日志中出现多次不同值,无法串联请求
原因:requestId在 Filter 中生成一次,但业务方法中又调用UUID.randomUUID().toString()生成新 ID,导致Result对象中的requestId与日志中的不一致。
解决:将requestId存入ThreadLocal,在 Filter 中生成并设置,在Result构造时从ThreadLocal读取:
public class RequestIdHolder { private static final ThreadLocal<String> REQUEST_ID = ThreadLocal.withInitial( () -> "req_" + UUID.randomUUID().toString().replace("-", "") ); public static String getRequestId() { return REQUEST_ID.get(); } public static void clear() { REQUEST_ID.remove(); } } // Filter 中 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { try { chain.doFilter(request, response); } finally { RequestIdHolder.clear(); // 必须清除,防 Tomcat 线程复用 } }6.5 现象:amount字段返回6800.00(数字),前端格式化后显示为6,800.00,但产品要求无小数位
原因:后端数据库字段为DECIMAL(10,2),ORM 框架(如 MyBatis)自动映射为BigDecimal,toString()返回6800.00。规范要求“显示性返回格式化字符串”,但后端未做setScale(0, RoundingMode.HALF_UP).toPlainString()处理。
解决:在 VO(View Object)中对金额字段做显式格式化:
public class UserVO { private String amount; // 不用 BigDecimal public void setAmount(BigDecimal amount) { this.amount = amount.setScale(0, RoundingMode.HALF_UP) .toPlainString(); } }7. 从规范到落地:我每天上线前必跑的 3 条验证命令与 1 个检查清单
7.1 用curl+jq一键验证核心契约
我把以下三条命令固化为check-api.sh,每次提测前在本地环境执行。它不依赖任何 UI,纯命令行验证,5 秒出结果:
#!/bin/bash # 检查1:空列表是否为 [] curl -s "http://localhost:8080/api/v1/acs/users/list_by_param?per_page=1" | jq -e '.data.list | type == "array"' > /dev/null || { echo "❌ 空列表未返回 []"; exit 1; } # 检查2:金额是否为字符串且含千分位 curl -s "http://localhost:8080/api/v1/acs/users/1" | jq -e '.data.amount | type == "string" and test("^\\d{1,3}(,\\d{3})*$")' > /dev/null || { echo "❌ 金额格式错误"; exit 1; } # 检查3:响应头 Content-Type 是否正确 curl -sI "http://localhost:8080/api/v1/acs/users/1" | grep -i "content-type: application/json; charset=utf-8" > /dev/null || { echo "❌ Content-Type 缺失"; exit 1; } echo "✅ 全部契约验证通过"7.2 Swagger 文档的“三不原则”检查清单
Swagger 是规范落地的第一道防线。我要求每个接口必须满足:
| 检查项 | 合规示例 | 违规示例 | 工具定位 |
|---|---|---|---|
不缺@ApiModel | @ApiModel(value="用户VO", description="用户视图对象") | 仅@Data无@ApiModel | Swagger UI → Model Schema |
不漏@ApiModelProperty | @ApiModelProperty("用户手机号,已脱敏") private String mobile; | 字段无注解,Swagger 显示mobile: string | Swagger UI → Model Properties |
不错@ApiResponse | @ApiResponse(code=200, message="操作成功", response=Result.class) | @ApiResponse(code=200, message="success")无response | Swagger UI → Responses |
提示:用
mvn compile后检查target/generated-sources/swagger下的 JSON 文件,搜索"type":"string"但无description的字段——这些就是遗漏注解的“暗雷”。
7.3 从Result<T>到前端 Hook 的类型穿透实践
后端Result<UserVO>的价值,只有穿透到前端才能兑现。我在 React 项目中封装了useApiHook:
// api/useApi.ts export function useApi<T>() { const request = useCallback(async (url: string) => { const res = await fetch(url); const result: Result<T> = await res.json(); // TypeScript 自动推导 T if (result.code !== 200) { throw new Error(result.message); // 精准捕获 403/400 等 } return result.data; // 类型为 T,非 any }, []); return { request }; } // 使用时 const { request } = useApi<UserVO>(); const user = await request("/api/v1/acs/users/1"); // user 类型为 UserVO,IDE 自动补全这套机制让后端Result<T>的泛型定义,100% 传导至前端变量类型。从那以后我每次写新接口,都先在 Swagger 里确认data字段类型是否正确,再生成 TS 定义,最后才写业务逻辑——因为类型错了,后面全是徒劳。希望帮到你。
本文还有配套的精品资源,点击获取