接口规范定义:关键不在于「怎么写」,而在于「怎么约定」
做接口设计这么多年,我最大的体会是:代码写得好不好是第二位,团队能不能按同一个套路说话,才是第一位。密钥权限审计这种敏感接口,一旦规范乱了,排查一个线上问题你可能会翻三天日志。这一章我们就把规范这件事彻底聊透。
Restful API 设计原则:资源是核心,动作是附属
RESTful 的本质是「面向资源」,不是面向动作。什么意思?你想想看:传统接口喜欢写成 /getPermissionList、/deleteAuditLog,动词满天飞。但在 REST 里,资源才是主体,HTTP 方法就是动词。
- 资源导向:每个 URL 代表一个资源,比如
/api/v1/permissions代表密钥权限集合,/api/v1/audit-logs代表审计日志集合。 - 无状态:每次请求都包含所有必要信息,服务端不存会话。我在项目里吃过亏——某个审计查询接口依赖 session 里的用户 ID,结果微服务拆分后 session 不共享,接口直接崩了。从那以后我再也不写有状态的 Rest 接口。
- 统一接口:增删改查分别对应 POST、DELETE、PUT/PATCH、GET,不要自己发明
/updatePermission这种路径。
URL 命名规范:看着路径就知道是干什么的
URL 是接口的「脸面」。我见过一个项目把路径写成 /api/perm/audit/list_all,大小写混用、下划线、复数单数混搭。嗯,维护半年后,连作者本人都不记得哪个路径是对的了。
# 推荐命名风格
GET /api/v1/permissions # 权限列表
GET /api/v1/permissions/{id} # 单个权限详情
POST /api/v1/permissions # 创建权限
PUT /api/v1/permissions/{id} # 全量更新权限
PATCH /api/v1/permissions/{id} # 部分更新权限
DELETE /api/v1/permissions/{id} # 删除权限
# 审计相关
GET /api/v1/audit-logs # 审计日志列表
GET /api/v1/audit-logs/{logId} # 单条日志详情
POST /api/v1/audit-logs # 写入审计日志
具体规则我总结成一张表,你直接照做就行:
| 规范项 | 推荐做法 | 反例 |
|---|---|---|
| 大小写 | 全小写 | /GetPermissions |
| 单词分隔 | 连词符(-) | /get_permissions 或 /GetPermissions |
| 集合名词 | 复数 | /permission(单数) |
| 版本号 | 路径前缀 /v1/ |
放到查询参数 ?version=1 |
| 过滤/排序 | 查询参数 | 路径里拼 /page/1/size/20 |
| 层级关系 | 嵌套表示从属 | 平铺在查询参数里 |
/v1/users/{uid}/roles/{rid}/permissions/{pid}),写到后面自己都晕。经验之谈——嵌套最多两层,超过两层就考虑拆成独立的资源查询或用查询参数传递父级ID。
请求/响应格式:JSON Schema 是「接口说明书」
光说「返回 JSON」是不够的。字段是什么类型?能否为空?范围多少?这些不定义清楚,前端和后端就会互相甩锅。JSON Schema 就是用来焊死这个约定的工具。
请求体示例(创建密钥权限)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CreatePermissionRequest",
"type": "object",
"required": ["keyId", "principalId", "actions"],
"properties": {
"keyId": {
"type": "string",
"description": "密钥唯一标识",
"pattern": "^km-[a-zA-Z0-9]{16}$"
},
"principalId": {
"type": "string",
"description": "主体ID(用户/角色/应用)",
"minLength": 1,
"maxLength": 64
},
"principalType": {
"type": "string",
"enum": ["user", "role", "app"],
"default": "user"
},
"actions": {
"type": "array",
"items": {
"type": "string",
"enum": ["encrypt", "decrypt", "sign", "verify"]
},
"minItems": 1,
"uniqueItems": true
},
"expiresAt": {
"type": "string",
"format": "date-time",
"description": "权限过期时间,不传表示永不过期"
}
},
"additionalProperties": false
}
响应体示例(审计日志列表)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuditLogListResponse",
"type": "object",
"required": ["items", "total", "page", "pageSize"],
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"logId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"action": { "type": "string" },
"actor": { "type": "string" },
"resourceType": { "type": "string" },
"resourceId": { "type": "string" },
"result": { "type": "string", "enum": ["success", "denied", "error"] },
"details": { "type": "object" }
},
"required": ["logId", "timestamp", "action", "actor", "result"]
}
},
"total": { "type": "integer", "minimum": 0 },
"page": { "type": "integer", "minimum": 1 },
"pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }
}
}
以前有个项目,接口返回了三个不同的日期格式,前端解析直接崩了。那时候我就想,要是有个「契约」能把字段类型、格式、取值范围全部写死……后来 JSON Schema 帮我解决了这个问题。你只要把 Schema 放到 CI 里做校验,接口发出去之前就知道对不对。
状态码约定:别什么情况都返回 200
我见过最离谱的接口——无论成功还是失败,HTTP 状态码永远是 200,然后自己在 body 里塞一个 code 字段。这样做等于把 HTTP 协议的精髓扔了。你想想看,客户端根本没法通过状态码快速判断是重试还是弹错误提示。
密钥权限审计场景,我推荐这么约定:
| 状态码 | 含义 | 典型场景 |
|---|---|---|
| 200 OK | 请求成功 | 查询成功、更新成功 |
| 201 Created | 创建成功 | 新建权限、写入审计日志 |
| 204 No Content | 删除成功(无返回体) | 删除权限 |
| 400 Bad Request | 请求参数有误 | 缺少必填字段、格式错误、枚举值不合法 |
| 401 Unauthorized | 未认证或认证过期 | 缺失或无效 |
| 403 Forbidden | 无权限执行此操作 | 非管理员试图删除他人权限 |
| 404 Not Found | 资源不存在 | 查询不存在的权限 ID |
| 409 Conflict | 资源冲突 | 创建已存在的权限记录 |
| 422 Unprocessable Entity | 业务校验失败 | 密钥已过期,无法授予新权限 |
| 429 Too Many Requests | 频率限制 | 审计查询接口触发限流 |
| 500 Internal Server Error | 服务端内部错误 | 数据库连接异常、未捕获异常 |
| 503 Service Unavailable | 服务暂时不可用 | 审计服务正在重启 |
整体规范流程概览
说了这么多,其实核心就一套闭环:资源→URL→请求体→响应体→状态码。下面这张图可以帮你理清脉络——我对接口进行规范定义时,脑子里默认就跳转这个流程。
小结:规范是接口的「安全绳」
说实话,规范定得再好,执行不到位也白搭。但反过来,没有规范,项目规模一大就会变成混沌状态。我个人建项目的第一步永远是拉一个 api-specs/ 目录,把 JSON Schema 和 OpenAPI 3.0 描述文件放进去,然后让 CI 跑校验。只有机器说通过了,我才合并代码。