全栈手账 订阅 RSS

接口设计规范

前后端协作的接口约定手册:涵盖 URL 与资源命名、HTTP 方法与状态码、统一响应结构、错误码体系、分页排序过滤、版本管理、幂等设计、接口文档与兼容性演进,可作为多项目复用的团队级标准。

读了 18 分钟 更新于 11 月 3 日

一、为什么需要接口规范

接口是前后端唯一的契约面。缺少规范会直接产生这些成本:

问题 后果
响应格式不统一 前端每个接口都要写不同的解析逻辑
错误码各自为政 无法统一拦截,错误提示五花八门
命名风格混乱(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,但不适合所有业务动作。以下场景用「子资源 + 动作」更清晰:

code
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 删除 ✅ ❌

两条硬规则:

  1. GET 不得产生副作用(不能删数据、不能下单)——这是缓存与 CSRF 防御的前提。
  2. 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 标准格式

json
{  "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 分页响应

json
{  "code": 0,  "message": "ok",  "data": {    "list": [],    "pagination": {      "page": 1,      "pageSize": 20,      "total": 137,      "totalPages": 7,      "hasMore": true    }  }}

同时提供 total 与 hasMore:前者用于分页器,后者用于无限滚动(避免深分页 count 开销)。

4.3 列表不带分页时

小规模固定数据(如字典、配置)直接返回数组:

json
{ "code": 0, "message": "ok", "data": ["a", "b"] }

但同一接口不要有时带分页有时不带。

4.4 前端统一拦截

响应结构统一的收益在这里体现——一次封装,全局可用:

javascript
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 集中定义,前后端共用

javascript
// 服务端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,}
javascript
// 前端(同一份内容,或通过代码生成)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 万行。优化方式:

sql
-- ❌ 深分页:越翻越慢SELECT * FROM orders ORDER BY id DESC LIMIT 100000, 20;-- ✅ 游标分页:始终走索引SELECT * FROM orders WHERE id < 100020 ORDER BY id DESC LIMIT 20;

6.2 排序

http
GET /api/products?sort=created_at&order=desc

必须白名单校验(防 SQL 注入):

javascript
const SORTABLE = ['created_at', 'price', 'sales']const sort = SORTABLE.includes(q.sort) ? q.sort : 'created_at'

6.3 过滤与搜索

http
# 精确过滤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 三种实现方式

方式一:业务唯一键(最可靠)

sql
-- 唯一索引兜底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 加锁/查重,成功后标记已用

方式三:状态机 + 条件更新

sql
-- ✅ 只有待支付状态才允许改成已支付UPDATE orders SET status = 'paid', paid_at = NOW()WHERE id = ? AND status = 'pending';-- 影响行数 0 → 说明已被处理,直接返回成功或提示

8.3 适合做幂等的场景

场景 必要性
下单、支付回调 ★★★★★
退款、提现 ★★★★★
发放优惠券/积分 ★★★★☆
创建资源(防重复提交) ★★★☆☆

九、接口文档

9.1 用 OpenAPI 保证文档与代码同步

手工维护的文档必然滞后。推荐由代码生成:

javascript
// NestJS + Swagger@ApiOperation({ summary: '查询用户列表' })@ApiQuery({ name: 'page', required: false, type: Number })@Get()findAll(@Query() dto: PageQueryDto) { ... }
yaml
# 或在设计阶段先写契约(契约先行)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

十二、总结

接口规范的价值不在「好看」,而在让协作可预测:

  1. 统一响应结构是收益最大的一条——前端一次封装覆盖全部接口。
  2. 错误码集中管理,前后端各存一份或代码生成,避免手抄漂移。
  3. 幂等设计是写接口时最容易漏、线上代价最高的一点,下单与支付类接口必须做。
  4. 只做兼容性新增,破坏性变更升版本,是保护旧客户端的最低成本方式。

最后一条经验:规范一旦定下,就写进项目脚手架与代码模板——只写在文档里、不落到模板中的约定,通常三个月后就会形同虚设。


完

前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。