接口设计规范
前后端协作的接口约定手册:涵盖 URL 与资源命名、HTTP 方法与状态码、统一响应结构、错误码体系、分页排序过滤、版本管理、幂等设计、接口文档与兼容性演进,可作为多项目复用的团队级标准。
一、为什么需要接口规范
接口是前后端唯一的契约面。缺少规范会直接产生这些成本:
| 问题 | 后果 |
|---|---|
| 响应格式不统一 | 前端每个接口都要写不同的解析逻辑 |
| 错误码各自为政 | 无法统一拦截,错误提示五花八门 |
命名风格混乱(userId / user_id) |
联调频繁出错,字段拼写错误难以发现 |
分页参数不统一(page / pageNum / offset) |
前端无法封装通用分页组件 |
| 无版本管理 | 改动一个字段就可能破坏线上旧版客户端 |
核心目标:让前端能写一次通用封装,覆盖 90% 的接口。
二、URL 与资源命名
2.1 基本规则
| 规则 | 正确 | 错误 |
|---|---|---|
| 资源用名词复数 | /api/users |
/api/getUsers、/api/user |
| 用 HTTP 方法表达动作 | DELETE /api/users/1 |
/api/users/delete?id=1 |
| 层级用路径表达 | /api/users/1/orders |
/api/orders?userId=1(归属关系用路径更清晰) |
| 全小写,多词用连字符 | /api/order-items |
/api/orderItems、/api/order_items |
| 不在 URL 放动词 | /api/users/1/activate(状态变更是例外) |
/api/activateUser |
| 统一前缀 | /api |
有的带有的不带 |
2.2 何时可以打破纯 REST
REST 适合资源 CRUD,但不适合所有业务动作。以下场景用「子资源 + 动作」更清晰:
POST /api/orders/1024/pay # 支付POST /api/orders/1024/cancel # 取消POST /api/users/1/password/reset # 重置密码POST /api/files/batch-delete # 批量删除判断标准:动作不是简单的资源状态替换,而是一个业务流程时,用动词子路径比硬套 REST 更可读。
三、HTTP 方法与状态码
3.1 方法语义
| 方法 | 语义 | 幂等 | 安全 |
|---|---|---|---|
| GET | 查询 | ✅ | ✅ |
| POST | 创建 / 执行动作 | ❌ | ❌ |
| PUT | 全量替换 | ✅ | ❌ |
| PATCH | 局部更新 | ⚠️ 取决于实现 | ❌ |
| DELETE | 删除 | ✅ | ❌ |
两条硬规则:
- GET 不得产生副作用(不能删数据、不能下单)——这是缓存与 CSRF 防御的前提。
- PUT 语义是全量替换:没传的字段应被置空。若只想改部分字段,用 PATCH 或 POST。
3.2 状态码使用
| 码 | 使用场景 | 说明 |
|---|---|---|
| 200 | 查询成功、更新成功 | 最常见 |
| 201 | 创建成功 | 建议同时返回 Location 头 |
| 204 | 成功但无返回体 | 删除成功常用 |
| 400 | 参数错误 / 校验失败 | 请求本身有问题 |
| 401 | 未认证(没登录、token 失效) | 前端应跳转登录 |
| 403 | 已认证但无权限 | 不应跳转登录 |
| 404 | 资源不存在 / 无权访问 | 越权时用 404 而非 403,避免泄露存在性 |
| 409 | 冲突(重复创建、状态不允许) | 配合业务错误码使用 |
| 422 | 语义校验失败 | 与 400 的界限团队内统一即可 |
| 429 | 请求过频 | 配合 Retry-After |
| 500 | 服务端异常 | 绝不返回堆栈 |
| 502/504 | 网关错误 / 上游超时 | 运维层问题 |
401 与 403 的区别是最常见的错误:401 是「你是谁不清楚」,403 是「知道你是谁,但不让你干」。
四、统一响应结构
4.1 标准格式
{ "code": 0, "message": "ok", "data": {}, "requestId": "b3f1c2a4-..."}| 字段 | 类型 | 说明 |
|---|---|---|
code |
number | 业务状态码,0 表示成功(也可用 200,但必须全局统一) |
message |
string | 面向用户或开发者的提示 |
data |
any | 业务数据;无数据时为 null 或 {},不要省略字段 |
requestId |
string | 请求追踪 ID,便于排查(生产环境强烈建议) |
关键约定:
- 字段名始终存在,即使为空——避免前端做
if (res.data && ...)的防御式判断。 data的类型保持稳定:列表接口固定返回数组或{ list, total },不要有时数组有时对象。
4.2 分页响应
{ "code": 0, "message": "ok", "data": { "list": [], "pagination": { "page": 1, "pageSize": 20, "total": 137, "totalPages": 7, "hasMore": true } }}同时提供
total与hasMore:前者用于分页器,后者用于无限滚动(避免深分页 count 开销)。
4.3 列表不带分页时
小规模固定数据(如字典、配置)直接返回数组:
{ "code": 0, "message": "ok", "data": ["a", "b"] }但同一接口不要有时带分页有时不带。
4.4 前端统一拦截
响应结构统一的收益在这里体现——一次封装,全局可用:
const request = axios.create({ baseURL: '/api', timeout: 10000 })request.interceptors.response.use( (res) => { const { code, message, data } = res.data if (code === 0) return data if (code === 401) { /* 跳登录 */ } if (code === 403) { /* 提示无权限 */ } return Promise.reject(new Error(message)) }, (err) => { const status = err.response?.status if (status === 401) location.href = '/login' return Promise.reject(err) })五、错误码体系
5.1 分层设计
0 成功4xx/5xx 与 HTTP 状态码对齐的通用错误1xxxx 用户模块2xxxx 订单模块3xxxx 支付模块...| 码 | 含义 | HTTP |
|---|---|---|
| 0 | 成功 | 200 |
| 400 | 参数错误 | 400 |
| 401 | 未登录 / 登录失效 | 401 |
| 403 | 无权限 | 403 |
| 404 | 资源不存在 | 404 |
| 500 | 服务器异常 | 500 |
| 10001 | 用户名已存在 | 409 |
| 10002 | 密码错误 | 400 |
| 20001 | 库存不足 | 409 |
| 20002 | 订单状态不允许此操作 | 409 |
| 30001 | 支付通道异常 | 502 |
5.2 集中定义,前后端共用
// 服务端export const ErrorCode = { SUCCESS: 0, PARAM_INVALID: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, NOT_FOUND: 404, SERVER_ERROR: 500, USER_EXISTS: 10001, PASSWORD_WRONG: 10002, STOCK_INSUFFICIENT: 20001,}// 前端(同一份内容,或通过代码生成)const ErrorMessage = { 10001: '用户名已存在', 10002: '密码错误', 20001: '库存不足',}建议把错误码表放在共享包或由 OpenAPI 生成,避免前后端手抄不一致。
5.3 错误码设计原则
| 原则 | 说明 |
|---|---|
| 一旦发布不可复用 | 同一个码永远表示同一种错误 |
| 不暴露内部细节 | 10001 而非 Duplicate entry 'admin' for key 'username' |
| message 面向用户 | 技术细节写日志,不写响应 |
| 一码一义 | 不要用 code=1 表示所有失败 |
六、请求参数规范
6.1 分页
统一参数名(三选一,团队内固定):
| 方案 | 参数 | 适用 |
|---|---|---|
| 页码分页 | page(从 1 开始)+ pageSize |
后台管理、总页数展示 |
| 偏移分页 | offset + limit |
需要精确跳转 |
| 游标分页 | cursor + limit |
无限滚动、大数据量、避免深分页 |
深分页问题:LIMIT 100000, 20 需要扫描 10 万行。优化方式:
-- ❌ 深分页:越翻越慢SELECT * FROM orders ORDER BY id DESC LIMIT 100000, 20;-- ✅ 游标分页:始终走索引SELECT * FROM orders WHERE id < 100020 ORDER BY id DESC LIMIT 20;6.2 排序
GET /api/products?sort=created_at&order=desc必须白名单校验(防 SQL 注入):
const SORTABLE = ['created_at', 'price', 'sales']const sort = SORTABLE.includes(q.sort) ? q.sort : 'created_at'6.3 过滤与搜索
# 精确过滤GET /api/orders?status=paid&userId=1024# 范围过滤GET /api/orders?createdAtFrom=2026-01-01&createdAtTo=2026-01-31# 关键词搜索(与过滤区分开)GET /api/products?keyword=phone# 多值筛选GET /api/products?status=on_sale,out_of_stock约定:数组参数统一用逗号分隔,不用重复 key(?id=1&id=2 在不同框架解析行为不一致)。
6.4 字段命名
| 层 | 约定 | 示例 |
|---|---|---|
| 数据库 | snake_case | created_at |
| 接口响应 | camelCase | createdAt |
| 前端变量 | 与接口一致 | createdAt |
数据库与接口用不同命名是常见做法,但必须在实体/序列化层显式映射,禁止靠"猜"。
6.5 时间与金额
| 类型 | 格式 | 说明 |
|---|---|---|
| 时间 | ISO 8601 字符串 2026-01-01T12:00:00+08:00,或 Unix 时间戳(秒) |
全局统一;不要混用字符串与数字 |
| 金额 | 整数分(price: 9900 表示 99.00 元)或字符串小数 |
禁用浮点数——0.1 + 0.2 !== 0.3 |
| 布尔 | true/false |
不用 0/1、"Y"/"N" 混用 |
| 枚举 | 字符串(语义清晰)或整数(紧凑) | 二选一,不要混 |
七、版本管理
7.1 三种方式
| 方式 | 示例 | 评价 |
|---|---|---|
| URL 路径 | /api/v1/users |
✅ 最直观、易缓存、易路由 |
| 请求头 | Accept: application/vnd.api.v2+json |
更符合 REST,但调试不便 |
| 查询参数 | /api/users?version=2 |
不推荐,污染参数空间 |
推荐 URL 路径版本,实现简单、排查直观。
7.2 何时升版本
| 变更 | 是否升版本 |
|---|---|
| 新增可选字段 | ❌ |
| 新增接口 | ❌ |
| 删除字段 | ✅ |
| 字段类型变更(string → number) | ✅ |
| 修改字段语义(含义变了) | ✅ |
| 修改错误码含义 | ✅ |
原则:只做兼容性新增,不做破坏性修改。破坏性变更走新版本,旧版本保留至少一个过渡期。
八、幂等性设计
8.1 为什么需要
网络超时后客户端重试,或用户连点两次,都可能导致重复下单、重复扣款。
| 方法 | 天然幂等 |
|---|---|
| GET / PUT / DELETE | ✅ |
| POST | ❌ 需显式设计 |
8.2 三种实现方式
方式一:业务唯一键(最可靠)
-- 唯一索引兜底ALTER TABLE orders ADD UNIQUE KEY uk_request (request_id);-- 插入时若冲突,说明是重复请求 → 返回已有结果INSERT INTO orders (request_id, user_id, ...) VALUES (?, ?, ...);方式二:幂等 Token
1. 客户端先请求 GET /api/idempotency-token 拿到 token2. 提交时带上 token3. 服务端以 token 为 key 加锁/查重,成功后标记已用方式三:状态机 + 条件更新
-- ✅ 只有待支付状态才允许改成已支付UPDATE orders SET status = 'paid', paid_at = NOW()WHERE id = ? AND status = 'pending';-- 影响行数 0 → 说明已被处理,直接返回成功或提示8.3 适合做幂等的场景
| 场景 | 必要性 |
|---|---|
| 下单、支付回调 | ★★★★★ |
| 退款、提现 | ★★★★★ |
| 发放优惠券/积分 | ★★★★☆ |
| 创建资源(防重复提交) | ★★★☆☆ |
九、接口文档
9.1 用 OpenAPI 保证文档与代码同步
手工维护的文档必然滞后。推荐由代码生成:
// NestJS + Swagger@ApiOperation({ summary: '查询用户列表' })@ApiQuery({ name: 'page', required: false, type: Number })@Get()findAll(@Query() dto: PageQueryDto) { ... }# 或在设计阶段先写契约(契约先行)paths: /api/v1/users: get: summary: 查询用户列表 parameters: - name: page in: query schema: { type: integer, minimum: 1, default: 1 } responses: '200': description: 成功 content: application/json: schema: $ref: '#/components/schemas/UserListResponse'9.2 必须写清楚的字段
| 项 | 说明 |
|---|---|
| 参数类型、是否必填、取值范围、默认值 | 前端据此做校验 |
| 示例请求与示例响应 | 比字段表更有用 |
| 错误码列表 | 前端据此做提示 |
| 鉴权要求 | 是否需要登录、需要什么角色 |
十、接口性能与安全约定
| 约定 | 说明 |
|---|---|
| 单接口响应 < 300ms(P95) | 超时则需异步化或加缓存 |
| 响应体不含无用字段 | 列表接口不要返回大文本/二进制 |
| 明确超时时间 | 前端 10s,服务端内部调用 3s |
| 限流 | 公开接口按 IP、登录接口按账号限流 |
| 敏感字段脱敏 | 手机号 138****0000,身份证同理 |
| 不在 URL 传敏感信息 | token、密码不进 query(会进日志与浏览器历史) |
| 批量接口限制条数 | 一次最多 100 条,防放大攻击 |
十一、检查清单
命名与结构
- 资源用名词复数,动作用 HTTP 方法表达
- 全小写 + 连字符,无下划线混用
- 所有接口统一
/api前缀 - 响应统一
{ code, message, data },字段始终存在
行为约定
- GET 无副作用
- 401(未认证)与 403(无权限)正确区分
- 越权访问返回 404 而非 403
- 保留
requestId便于排查 - 错误响应不含堆栈与 SQL
参数
- 分页参数名全站统一
- 排序字段白名单校验
- 数组参数用逗号分隔
- 时间格式统一(ISO 8601 或时间戳,二选一)
- 金额用整数分,不用浮点
演进与安全
- 破坏性变更走新版本
- 关键写操作(下单、支付、退款)有幂等设计
- 接口文档由代码生成或契约先行,不靠手写
- 敏感字段脱敏,敏感信息不进 URL
十二、总结
接口规范的价值不在「好看」,而在让协作可预测:
- 统一响应结构是收益最大的一条——前端一次封装覆盖全部接口。
- 错误码集中管理,前后端各存一份或代码生成,避免手抄漂移。
- 幂等设计是写接口时最容易漏、线上代价最高的一点,下单与支付类接口必须做。
- 只做兼容性新增,破坏性变更升版本,是保护旧客户端的最低成本方式。
最后一条经验:规范一旦定下,就写进项目脚手架与代码模板——只写在文档里、不落到模板中的约定,通常三个月后就会形同虚设。
完