RESTful API 设计原则

说到 API 设计,你可能觉得不就是写几个接口吗?其实没那么简单。我见过不少团队,接口设计随心所欲,结果联调时鸡飞狗跳。RESTful 风格不是银弹,但确实能帮你省掉很多沟通成本。

这一节,我们聊聊 RESTful API 的几个核心点。资源命名、HTTP 动词、状态码、版本管理,还有文档生成。每个点我都踩过坑,咱们一个一个拆开说。

资源命名:用名词,别用动词

REST 的核心是资源。资源就是名词,不是动作。你看很多老系统,接口长这样:/getUserInfo、/createOrder。这其实不是 RESTful。

RESTful 的思路,是把所有东西都当成资源。用户、订单、模型、任务,都是资源。用名词复数表示资源集合,用路径层级表示从属关系。

举个例子。我有一个模型推理服务,资源设计可以这样:

// 模型资源
GET    /models                // 获取模型列表
POST   /models                // 部署新模型
GET    /models/{model_id}     // 获取单个模型详情
DELETE /models/{model_id}     // 删除模型

// 推理任务资源
POST   /models/{model_id}/inferences   // 触发推理
GET    /inferences/{task_id}           // 查询推理结果

注意几个细节。第一,统一用小写连字符,不用下划线。第二,层级不要太深,超过三级就该考虑拆分。我习惯把嵌套限制在三级以内,再深就拆成子资源。

命名的几个硬规矩

  • 只用名词复数(/models,/users)
  • URI 中用连字符,不用下划线驼峰
  • 层级最深不要超三级
  • 避免在路径里用动词(别写 /getModel)

HTTP 动词:CRUD 的天然映射

HTTP 动词就是一套标准操作。GET、POST、PUT、PATCH、DELETE,正好对应资源的增删改查。这不是我发明的,但很多团队没用好。

看看这张对应关系:

HTTP 动词 操作 是否幂等 典型场景
GET查询是获取资源
POST创建否新建模型
PUT全量更新是替换整个资源
PATCH部分更新否修改模型配置
DELETE删除是移除资源

这里有个常见误区:PUT 和 PATCH 的区别。我见过不少团队全部用 PUT,结果部分更新时传入完整对象,既浪费带宽又容易出错。说实话,PATCH 才是部分更新的正确选择。

我个人的习惯

对于批量操作,用 POST 带上一个 action 参数。比如批量删除:

POST /models/batch-delete
{ "model_ids": ["m1","m2","m3"] }

虽然不符合纯 REST 规范,但实践中更直观。

状态码:别只返回 200 和 500

很多项目接口返回永远只有 200 和 500。你想想看,前端拿到 200 还要去看 body 里到底是成功还是失败,这不是浪费吗?

HTTP 状态码本身就是信号,咱得用起来。

范围含义常用码说明
2xx成功200 201 204200 正常,201 创建成功,204 删除成功
3xx重定向301 302 307很少在 API 中使用
4xx客户端错误400 401 403 404400 参数错,401 未认证,403 无权限,404 资源不存在
5xx服务端错误500 502 503500 内部错,502 网关错,503 服务不可用

我记得有一次排查线上问题,对方接口永远返回 200,结果 body 里用 code 字段区分状态。日志查起来特别费劲。从那以后,我坚持状态码和业务含义一一对应。

当然,状态码也不用全背。记住常用的十个左右就够了。核心原则:2xx 表示请求已处理,4xx 表示你该改代码,5xx 表示我服务器该修了。

曾经踩过的坑

我刚写 API 时,喜欢在 400 里返回各种业务校验错误,比如"密码长度不够"、"邮箱格式错误"。后来发现这样做客户端很难统一处理。现在我习惯:

  • 参数格式问题用 400
  • 权限问题用 403
  • 资源不存在用 404
  • 冲突(如重复创建)用 409

这样前端可以根据状态码走不同分支,不用解析 body 里的错误类型。

版本管理:没有版本号,就是给自己挖坑

接口一定会变。加字段、改字段、删字段,总得经历。你不能指望所有客户端同步更新。所以版本号是必需品。

版本管理的常见方式有两种:URI 版本号和请求头版本号。

方式一:URI 版本号

GET /api/v1/models
GET /api/v2/models
POST /api/v1/models

这种方式最直观,我推荐新人用。一目了然,调试方便。

方式二:请求头版本号

GET /api/models
Accept: application/vnd.myapp.v1+json
Accept: application/vnd.myapp.v2+json

请求头方式更符合 REST 哲学,路径干净,适合大型网关场景。但调试麻烦,curl 要加额外头。

我个人习惯,在微服务内部用请求头版本号,对外公开 API 用 URI 版本号。对外接口简单直接最重要。

版本管理原则

  • 语义版本控制(MAJOR.MINOR.PATCH)
  • 不兼容变更必须升大版本
  • 小版本至少兼容 6 个月
  • 旧版本标注弃用时间(Deprecation 头)

文档生成:OpenAPI + Swagger 实战

你以为只要做好服务端就得了吗?接口变了,文档没更新,联调时一样会吵架。我见过不少团队盯着源码看接口参数,效率极低。

OpenAPI(原来叫 Swagger)标准现在基本是业界标配。写代码的时候就把文档一起生成了,省去额外维护的精力。

我用 FastAPI 举例,它默认集成 OpenAPI。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="模型推理服务", version="v1")

class ModelConfig(BaseModel):
    model_id: str
    max_batch: int = 32
    gpu_ids: list[int]

@app.post("/models", status_code=201)
async def deploy_model(config: ModelConfig):
    """部署新模型到推理引擎"""
    return {"status": "ok", "model_id": config.model_id}

运行后访问 /docs 就能看到交互式 Swagger UI。参数、返回值、状态码、错误说明,全部自动生成。

如果现有项目是 Flask 或 Django,也能通过装饰器方式集成。比如 Flask 项目:

from flask_openapi3 import OpenAPI, Info
from pydantic import BaseModel

info = Info(title="推理服务 API", version="v1")
app = OpenAPI("myapp", info=info)

class QueryModel(BaseModel):
    model_id: str

@app.get("/models/{model_id}")
def get_model(path: QueryModel):
    """查询模型详情"""
    return {"model_id": path.model_id, "status": "running"}

if __name__ == "__main__":
    app.run(debug=True)

我建议在 CI/CD 流程里加一道检查:如果当前分支改了 API 签名,但没更新 OpenAPI 文档,CI 直接报红。这能保证文档和代码始终同频。

文档里的好习惯

  • 每个接口写清楚作用、业务场景
  • 枚举字段给出可用值列表
  • 错误返回给出示例和解决方案
  • 标注接口的版本、作者、变更历史

看似麻烦,实际能省掉大量的群聊和会议。

知识体系总览

下面这张图把 RESTful API 设计的关键环节串起来了。从资源设计到文档生成,每个环节都有对应的原则和工具。你看一眼就能知道设计接口时该考虑哪些点。

RESTful API 设计知识体系 RESTful API 设计 资源命名 用名词复数 HTTP 动词 GET/POST/PUT/PATCH/DELETE 状态码 200/201/400/404/500 版本管理 URI 版本 / 请求头版本 文档生成 OpenAPI / Swagger 自动生成交互式文档 请求/响应模型校验 CI 中校验文档一致性 核心思路:用标准化的方式设计、沟通和维护 API

以上就是 RESTful API 设计的五个关键点。说白了,就是把你脑子里的业务逻辑翻译成标准接口语言,让前后端、微服务之间能顺畅沟通。下一个环节,我们会把这些原则应用到实际的多 GPU 推理服务中,设计出一套可扩展、可维护的 API 体系。