Skip to content

实战:博客系统 API 设计

本章导读:把前 5 篇的知识收敛成一份可评审的 API 设计文档。目标系统是一个中型博客平台:文章、评论、标签、用户、认证、搜索与导出。按"需求 → 资源建模 → 端点清单 → 契约细节 → 治理策略"五步走,这也是团队落地的真实工作流。

1. 需求清单(设计输入)

#需求涉及规范章节
R1作者可创建/编辑/发布/撤回/删除文章,编辑不能覆盖别人的修改方法语义、幂等、条件请求
R2读者可对文章发表评论(带分页),作者可删除旗下文章评论资源层次
R3文章可打标签;按标签/作者/状态/时间筛选,按时间/热度排序URI 规范、分页过滤排序
R4注册/登录/登出,第三方"用 GitHub 账号登录"认证与授权
R5全文搜索URI 规范、分页
R6导出全部文章为 CSV(万级数据,分钟级耗时)异步作业
R7文章封面图上传媒体类型、作业
R8对外开放 API,需限流与版本治理限流、版本

2. 资源建模

识别资源(四问法):

articles(集合)            /v1/articles
  └ article(单资源)        /v1/articles/{articleId}
      ├ comments(子集合)    /v1/articles/{articleId}/comments      ← 从属建模(权限沿文章继承)
      │   └ comment          /v1/comments/{commentId}               ← 单资源扁平化(后台治理需要)
      ├ draft(单例子资源)   /v1/articles/{articleId}/draft         ← 独立权限+独立编辑生命周期 → 拆
      ├ thumbnail            /v1/articles/{articleId}/thumbnail     ← 独立高频读 → 拆
      └ publish(状态迁移)   PUT /v1/articles/{id}/status           ← "发布"名词化为状态资源
tags(集合)                /v1/tags
users / sessions            /v1/users, /v1/sessions                ← 登录态资源化
searches(一次搜索=资源)    /v1/searches                           ← 复杂查询 noun 化
exports / jobs(作业)       /v1/jobs/{jobId}                        ← 异步执行统一出口

裁决记录(评审要写的"为什么"):

  • 评论用从属集合 + 扁平单资源混合:挂在文章下便于权限与分页,后台按 ID 直接治理;
  • "发布"不设计 POST /articles/{id}/publish:发布=状态迁移,可逆(撤回),用 PUT /status 幂等且天然支持"撤回"(写回 draft);
  • 搜索独立资源:查询串可超长、结果可翻页复用、便于按"一次搜索"计量与缓存。

3. 端点清单(API Surface)

3.1 认证(R4)

POST   /v1/sessions                 登录 → 201 + Location: /v1/sessions/cur,返回 token 对
DELETE /v1/sessions/cur             登出 → 204
POST   /v1/sessions/refresh         用 refreshToken 换新 access → 200
GET    /v1/oauth/authorize          → 302 到 GitHub(OAuth 授权码+PKCE)
GET    /v1/sessions                 我的会话列表(设备管理)
DELETE /v1/sessions/{id}            吊销指定会话 → 204

3.2 文章(R1)

GET    /v1/articles                 列表(分页/过滤/排序)→ 200
POST   /v1/articles                 创建草稿 → 201 + Location        [Idempotency-Key 必需]
GET    /v1/articles/{id}            详情 → 200 | 404 | 410
PUT    /v1/articles/{id}            替换可写属性 → 200 + 新 ETag      [If-Match 必需 → 412]
DELETE /v1/articles/{id}            删除(软删,保留 30 天)→ 204;此后 GET → 410
GET    /v1/articles/{id}/draft      草稿表示 → 200 | 404
PUT    /v1/articles/{id}/draft      覆盖草稿 → 200                    [If-Match 必需]
PUT    /v1/articles/{id}/status     发布/撤回:{"status":"published"} → 200 | 409(非法迁移)
POST   /v1/articles/{id}/thumbnail  上传封面 multipart → 201          (R7)
GET    /v1/articles/{id}/thumbnail  → 200 image/webp | 301 对象存储签名 URL

3.3 评论与标签(R2/R3)

GET    /v1/articles/{id}/comments   分页(游标)→ 200
POST   /v1/articles/{id}/comments   发表 → 201                        [Idempotency-Key]
PATCH  /v1/comments/{cid}           本人编辑 → 200 | 403
DELETE /v1/comments/{cid}           作者删自己评论 / 文章作者删旗下评论 → 204 | 403
GET    /v1/tags?prefix=             标签词表 → 200
GET    /v1/articles?tag=rest        按标签筛文章(标签是过滤参数不是路径)

3.4 搜索与导出(R5/R6)

POST   /v1/searches                 {"query":"...","filters":{…}} → 201 + Location: /v1/searches/{sid}
GET    /v1/searches/{sid}?cursor=   翻页取结果 → 200                  ← 结果缓存 60s
POST   /v1/exports                  {"type":"articles.csv"} → 202 + Location: /v1/jobs/{jid}
GET    /v1/jobs/{jid}               进度 → 200(含 Link: rel="result" 指向下载资源)
GET    /v1/files/{fid}              下载产物 → 200 text/csv(签名 URL 301)

4. 契约细节(选摘)

4.1 文章列表

http
GET /v1/articles?status=published&tag=rest&sort=-viewCount,createdAt&page=2&pageSize=20 HTTP/1.1
Accept: application/json
Authorization: Bearer …

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=30, stale-while-revalidate=60
Vary: Accept

{
  "items": [
    {
      "id": "42",
      "title": "RESTful API 设计规范",
      "excerpt": "……",
      "status": "published",
      "tagIds": ["t1", "t9"],
      "authorId": "7",
      "viewCount": 1024,
      "createdAt": "2026-09-18T08:30:00Z",
      "updatedAt": "2026-09-18T10:12:31Z",
      "links": { "self": "/v1/articles/42", "author": "/v1/users/7", "comments": "/v1/articles/42/comments" }
    }
  ],
  "page": 2, "pageSize": 20, "total": 214,
  "links": { "next": "/v1/articles?page=3&…", "prev": "/v1/articles?page=1&…" }
}

约定:列表不含 content(正文走 ?fields=content 或详情端点——防 over-fetching)。

4.2 发布(状态机 + 冲突)

状态机:draft → inReview → published → archived;published ⇄ draft(撤回)

PUT /v1/articles/42/status
{ "status": "published" }

200 → { "id":"42","status":"published","publishedAt":"…","version":8 }
409 → article-in-review:审核中不可直接发布(业务规则冲突,problem+json)
412 → 状态已变化(若启用 If-Match)

4.3 错误目录(节选)

type(错误码)HTTP触发端点
validation-failed422全部写端点
article-title-too-long422POST/PUT articles
invalid-status-transition409PUT status
version-conflict412PUT 带旧 ETag
invalid-cursor400列表翻页
permission-denied403评论/草稿越权
rate-limit-exceeded429全局(Retry-After)

5. 治理策略(写进文档的"宪法")

主题决策
命名camelCase;集合复数;URI 全小写 kebab
时间RFC 3339 UTC
ID文章/评论/用户对外 UUIDv7(字符串);禁自增
版本/v1;新增字段不升版本;未知枚举容忍
分页默认page=1, pageSize=20, max=100;评论用游标
认证Bearer JWT(15min)+ Refresh(30d 旋转);/articles 的 GET 匿名可访公开文章
幂等全部 POST 需 Idempotency-Key(24h 重放窗口)
并发文章/草稿写强制 If-Match;其余后写覆盖
缓存写响应/错误 no-store;公开 GET 短公共缓存 + 强 ETag 全资源启用
弃用Deprecation/Sunset 头 + 12 个月窗口
限流默认 1000 req/h/用户,写端点 200 req/h;Retry-After + 配额头部

6. 评审自查(走完这 8 问才算设计完成)

  1. 遮住 body,方法+URI 能否读懂 90% 意图?
  2. 每个 POST 是否想过"能否名词化/幂等化"?
  3. 集合接口是否全部强制分页?
  4. 写接口是否定义并发策略(If-Match / last-write / 409)?
  5. 错误是否落到 problem+json + type 目录,且无 5xx 细节泄露?
  6. 私有数据缓存指令是否全部 private/no-store
  7. 枚举、字段、错误码的演进规则是否成文?
  8. 是否给出 OpenAPI 文件(下一章生成)?

7. 下一步

设计稿完成,用 Spring Boot 把它变成可运行代码 → 实战:Spring Boot 实现