跳到主要内容

API 设计:从"能跑"到"好用"

很多人写接口的顺序是:先写 handler,再随手拼一个路径,POST /createUserGET /getUserList 满天飞。项目一多,前端同学就得挨个问:"这个接口路径是啥?传参放 query 还是 body?删错了返回啥?"

goddd 的答案是:定一套简单固定的规矩,所有接口都长一个样。这套规矩参考了 Google API 设计指南,但做了大幅简化,普通项目照着抄就行。本文把这套规范从头到尾讲一遍。

一、先立四条原则

  1. 资源导向——先想清楚你在操作什么"东西"(用户、订单、文章),再考虑对它做什么操作。
  2. 一致性——同样的语义,就用同样的 HTTP 方法、路径模式和状态码,不搞特例。
  3. 简单优先——标准方法能解决的,绝不发明新写法。
  4. 避免 PATCH——PUT 全量替换加上语义化路径已经足够表达,少一种方法就少一分歧义。

二、路径怎么起:资源命名

路径是给"资源"起的名字,不是给"动作"起的。规则只有四条:

规则正确错误
集合用复数名词/users/user
多单词用 kebab-case/user-profiles/userProfiles
标准方法里不放动词POST /usersPOST /users/create
嵌套最多 2 层/users/{id}/orders/{oid}/a/{id}/b/{bid}/c/{cid}

路径段用清晰的英文复数名词,避免 itemsobjects 这种说了等于没说的笼统词。

三、五个标准方法,覆盖九成场景

goddd 只用五个标准方法(注意:没有 PATCH):

方法HTTP路径参数绑定响应体
ListGET/resourcesform tag → Query{"items": [...], "total": N}
GetGET/resources/:iduri tag → 路由参数资源对象
CreatePOST/resourcesjson tag → Body创建后的完整资源
UpdatePUT/resources/:iduri + json tag → Body更新后的完整资源
DeleteDELETE/resources/:iduri tag

为什么 Update 用 PUT 全量替换而不是 PATCH 局部更新?因为"局部更新"意味着客户端要关心字段级别的差异,前端传漏一个字段就出诡异 bug。全量替换简单粗暴:你看到的表单是什么样,提交上来就是什么样。

Delete 还有一个贴心设计——幂等:资源已经被删过了,再删一次照样返回成功,而不是报错。重试、双击、网络重发都不会出问题。

配套的输入结构体直接靠 struct tag 声明绑定来源:

type ListEntityInput struct {
web.PagerFilter
web.DateFilter
Name string `form:"name" binding:"max=12"`
}

type CreateEntityInput struct {
Name string `json:"name" binding:"required,max=50"`
TenantID string `json:"-"`
CreatedBy string `json:"-"`
}

form 表示从 query 取,json 表示从 body 取,json:"-" 的字段(如租户 ID)由服务端自己填,不信任客户端传入。

四、标准方法不够时:自定义方法

有些操作确实塞不进增删改查,比如"取消订单"。这时用自定义方法,统一走 POST(只读查询可用 GET),路径里带一个语义化的动词:

场景路径示例
取消POST /:id/cancel
复杂搜索GET /search
撤销删除POST /:id/undelete

注意顺序:先确认标准方法真的表达不了,再上自定义方法。

五、错误处理:一律 400,两个例外

这是最容易引起争论的一条,但用起来真香:业务正常返回 200,业务出错一律返回 400,只有认证失败(401)和限流(429)用其它状态码

为什么不分 404、409、422?因为对调用方来说,"这个请求没成功"才是第一要义,具体原因应该由错误体里的 reason 字段精确表达,而不是让前端去背一堆状态码的含义。

项目里的写法:

// 默认 400 — 绝大多数业务错误
reason.ErrBadRequest.WithMsg("参数不合法")
reason.ErrNotFound.WithMsg("资源未找到")

// 显式 401 — 认证失败
reason.ErrUnauthorizedToken.WithMsg("用户已过期") // WithHTTPStatus(401)

// 显式 429 — 限流
reason.ErrRateLimit.WithMsg("请求频率过高") // WithHTTPStatus(429)

三个链式方法的分工:

  • WithMsg() — 给用户看的友好提示
  • With() / Withf() — 给开发者看的排查详情,正式环境(SetRelease())自动隐藏
  • WithHTTPStatus() — 覆盖默认的 400

客户端收到的错误响应长这样:

{"reason": "ErrBadRequest", "msg": "名称不能为空", "details": ["field[name]: 空字符串"], "trace_id": "abc123"}

前端按 reason 做分支处理,用户看 msg,开发者拿 trace_id 去日志里定位,各司其职。更完整的错误处理机制见「一小时学完框架设计」中的错误处理篇。

六、分页与过滤:两个结构体搞定

列表接口的分页、排序、时间过滤,不需要每个接口重写一遍。嵌入 web.PagerFilterweb.DateFilter 两个结构体即可:

type ListEntityInput struct {
web.PagerFilter
web.DateFilter
Name string `form:"name"`
}

请求:

GET /entities?page=1&size=20&sort=-created_at&start_ms=1720000000000

响应:

{"items": [...], "total": 42}

几个要点:

  • 排序字段走 SortSafelist 白名单,防止 SQL 注入;- 前缀表示降序,+ 表示升序
  • DateFilter 用毫秒时间戳,StartAt() / EndAt() 可以直接拿到 time.Time
  • 想一次性全量查询,用 NewPagerFilterMaxSize()

七、校验:格式管格式,业务管业务

校验分两层,各管各的:

绑定层——Gin 的 binding tag 管格式,声明在结构体上,绑定失败 WrapH 自动返回 400 并定位到具体字段:

tag作用
binding:"required"必填
binding:"min=1,max=100"数值范围
binding:"oneof=active inactive"枚举值
binding:"email"邮箱格式
binding:"max=255"最大长度

业务层——web.Validator 管业务规则(唯一性、是否存在、权限、状态):

v := web.NewValidator()
v.Check(in.Name != "", "name", "名称不能为空")
if !v.Valid() {
return nil, reason.ErrBadRequest.With(v.List()...)
}

一句话分工:binding 管"这个字段合不合法",Validator 管"这个操作合不合理"。

八、限流与路由模板

限流有三种粒度,按需要挂中间件:

中间件粒度
web.RateLimiter(r, b)全局
web.IPRateLimiterForGin(r, b)按 IP
web.IDRateLimiter(r, b, ttl)按用户 ID

被限流的请求返回 429 并带 Retry-After 头,客户端照着等就行。

最后,一个领域模块的路由注册就是固定模板,照抄改名即可:

func registerEntity(r gin.IRouter, api EntityAPI, handler ...gin.HandlerFunc) {
g := r.Group("/entities", handler...)
g.GET("", web.WrapH(api.listEntities))
g.POST("", web.WrapH(api.createEntity))
g.GET("/:id", web.WrapH(api.getEntity))
g.PUT("/:id", web.WrapH(api.updateEntity))
g.DELETE("/:id", web.WrapH(api.deleteEntity))
g.PUT("/sort", web.WrapH(api.sortEntities))
}

这样做的好处

  • 前端不用猜:所有接口路径、传参方式、错误结构完全一致,看一个就会全部。
  • AI 生成代码不偏:规矩是死的,AI 生成的接口天然符合规范,不用人工纠偏。
  • review 只看业务:参数绑定、校验、错误响应都由框架兜底,代码评审只需关注业务逻辑本身。

想深入可以参考 Google API 设计指南,goddd 的规范是它的精简实践版。

关于 goddd

goddd 是一个 AI 驱动的 Go 语言脚手架,基于领域驱动设计(DDD)思想搭建,提供了一套适合中小项目快速启动的工程结构和最佳实践。

如果你想了解更多细节,可以访问: