接口规范定义:关键不在于「怎么写」,而在于「怎么约定」

做接口设计这么多年,我最大的体会是:代码写得好不好是第二位,团队能不能按同一个套路说话,才是第一位。密钥权限审计这种敏感接口,一旦规范乱了,排查一个线上问题你可能会翻三天日志。这一章我们就把规范这件事彻底聊透。

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?
以前有个项目,接口返回了三个不同的日期格式,前端解析直接崩了。那时候我就想,要是有个「契约」能把字段类型、格式、取值范围全部写死……后来 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 服务暂时不可用 审计服务正在重启
坑,我踩过的那种:曾经有一个审计查询接口,在密钥不存在时返回了 400,但参数本身是合法的。前端按 400 提示「参数错误」,用户就懵了。正确做法:密钥不存在应该返回 404,让客户端知道「资源不存在」,而不是「你参数写错了」。

整体规范流程概览

说了这么多,其实核心就一套闭环:资源→URL→请求体→响应体→状态码。下面这张图可以帮你理清脉络——我对接口进行规范定义时,脑子里默认就跳转这个流程。

RESTful API 规范设计流程 RESTful API 规范设计流程 ① 识别资源 密钥/权限/审计日志 ② 定义 URL /v1/permissions/{id} ③ JSON Schema 请求/响应格式契约 ④ 状态码约定 200/201/400/403/404... ⑤ 客户端处理 重试/提示/跳转 规范设计的核心闭环:资源 → URL → 数据契约 → 状态码 → 客户端反馈 → 回到资源 每个环节都影响上下游,任何一个地方出问题都会让对接方抓狂

小结:规范是接口的「安全绳」

说实话,规范定得再好,执行不到位也白搭。但反过来,没有规范,项目规模一大就会变成混沌状态。我个人建项目的第一步永远是拉一个 api-specs/ 目录,把 JSON Schema 和 OpenAPI 3.0 描述文件放进去,然后让 CI 跑校验。只有机器说通过了,我才合并代码。