首页
/ realworld 后端 API 响应格式规范:六大资源 JSON 结构详解与 Hurl 测试验证实践

realworld 后端 API 响应格式规范:六大资源 JSON 结构详解与 Hurl 测试验证实践

2026-09-04 22:10:51作者:申梦珏Efrain

本文围绕 realworld 官方文档 docs/src/content/docs/specifications/backend/api-response-format.md 展开,完整解读该 Medium 风格全栈应用对后端实现提出的响应格式契约:User、Profile、Article、Comment、Tags 六类 JSON 对象的结构、可空字段语义与时间戳格式;并结合 OpenAPI 规范 中的 schema 定义和 Hurl 测试套件 的真实断言,说明每一条格式要求是如何被自动化测试逐字段校验的。读完本文,你可以对照规范实现自己的后端,并用仓库自带的 Hurl 用例逐条验证响应结构是否合规。

契约地位:文档只是摘要,测试才是真相

后端规范入口文档 中明确声明:各文档页面只是对契约的文字摘要,真正定义契约的是 OpenAPI 规范Hurl 测试套件——当文字描述与测试不一致时,以测试为准。因此本文在逐条解读响应结构的同时,均附上 Hurl 断言作为可执行依据。

所有后端实现首先必须满足的通用要求:

  • Content-Type 必须正确:响应需返回 Content-Type: application/json; charset=utf-8。这是 api-response-format.md 对 "JSON Objects returned by API" 的第一条硬性要求。
  • 顶层包装键(envelope key)固定:每个成功响应都用资源名单数或复数作为唯一顶层键,如 userprofilearticlearticlescommentcommentstags。这一命名与 OpenAPI 中各 Response schema 的 required 字段一一对应,例如 UserResponse 要求顶层必含 userMultipleArticlesResponse 要求必含 articlesarticlesCount(见 openapi.ymlcomponents.responses 的定义)。
  • 字段命名采用 camelCase:如 tagListcreatedAtfavoritesCount,不要用 snake_case。
  • 时间戳采用 ISO 8601(date-time):OpenAPI 中 createdAt/updatedAt 均声明为 format: date-time,例如 2016-02-18T03:22:56.637Z。Hurl 测试通过正则 ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2} 来校验该格式。
  • 可空字段返回 JSON null 而非缺省或空字符串bioimage 等字段在 User 和 Profile 对象中允许为 null(OpenAPI 中声明为 type: [string, 'null'])。

User:认证相关响应对象

用于注册(POST /api/users)、登录(POST /api/users/login)、获取当前用户(GET /api/user)和更新用户(PUT /api/user)四类端点,标准响应如下:

{
  "user": {
    "email": "jake@jake.jake",
    "token": "jwt.token.here",
    "username": "jake",
    "bio": null,
    "image": null
  }
}

结合 OpenAPI 的 User schema 可以确认字段要求:

字段 类型 必填 说明
email string 用户邮箱
token string JWT 令牌,用于后续受保护请求的 Authorization: Token xxx
username string 用户名
bio string | null 个人简介,可为 null
image string | null 头像 URL,可为 null

三个字段语义要点,均来自 Hurl 测试 auth.hurl 的实际断言:

  1. bioimage 未设置时必须是 null:注册和登录响应的断言为 $.user.bio == null$.user.image == null,而不是省略该键。
  2. 空字符串会被规范化为 null:Hurl 中专门有 "Update user bio to empty string - should normalize to null" 用例,PUT /api/user 提交 bio: "" 后断言 $.user.bio == null,且后续 GET /api/user 验证该规范化结果被持久化。image 同理。
  3. token 必须是非空字符串:断言为 $.user.token isString$.user.token not isEmpty。测试流程正是靠这一字段做后续请求的鉴权([Captures] reg_token: jsonpath "$.user.token")。

Profile:用户公开资料对象

Profile 是 User 的"对外视图"——不包含 emailtoken,额外增加 following 布尔量:

{
  "profile": {
    "username": "jake",
    "bio": "I work at statefarm",
    "image": "https://api.realworld.io/images/smiley-cyrus.jpg",
    "following": false
  }
}

对应端点:获取资料(GET /api/profiles/{username},鉴权可选)、关注(POST /api/profiles/{username}/follow)、取关(DELETE /api/profiles/{username}/follow,鉴权必须)。OpenAPI 中 Profile schema 的必填字段为 biofollowingimageusername,其中 bioimage 允许 nullfollowing 为 boolean。

Profile 还以嵌套对象形式出现在 Article 和 Comment 中(见下文的 author 字段),其 following 值取决于当前请求是否携带用户身份:未鉴权时通常为 false。这意味着你的后端在渲染文章、评论列表时,需要按"当前观察者"计算 following,而不是直接复存数据库值。

Single Article:单篇文章响应对象

文章创建(POST /api/articles,201)、读取(GET /api/articles/{slug},200)、更新(PUT /api/articles/{slug},200)、收藏/取消收藏(POST|DELETE /api/articles/{slug}/favorite,200)均返回该结构:

{
  "article": {
    "slug": "how-to-train-your-dragon",
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "body": "It takes a Jacobian",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }
}

OpenAPI Article schema 的必填字段为:authorbodycreatedAtdescriptionfavoritedfavoritesCountslugtagListtitleupdatedAt。几个容易被实现者忽略的行为,articles.hurl 都有对应断言:

  • tagList 顺序保留且是字符串数组:创建时提交的 ["d_xxx", "t_xxx"] 在响应中被断言为 tagList[0] == "d_xxx"tagList[1] == "t_xxx"
  • 新建文章初始状态favorited == falsefavoritesCount == 0author.username 等于创建者。
  • 更新时间戳语义:更新正文后断言 $.article.createdAt 保持不变、$.article.updatedAt 发生变化(测试先 [Captures] 捕获创建时的两个时间戳,再在 PUT 后比对)。
  • 更新时省略 tagList 则保留原标签:仅提交 bodyPUT 请求后,断言 tagList 仍为 2 项且内容不变;而显式提交 tagList: [] 表示清空,tagList: null 则应被拒绝(Hurl 断言该请求返回 422)。

Multiple Articles:文章列表响应对象与 body 移除规则

列表端点 GET /api/articles(支持 tag/author/favorited 过滤参数及 offset/limit 分页)与 GET /api/articles/feed 返回如下结构:

{
  "articles": [{
    "slug": "how-to-train-your-dragon",
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }, {
    "slug": "how-to-train-your-dragon-2",
    "title": "How to train your dragon 2",
    "description": "So toothless",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }],
  "articlesCount": 2
}

关键变更(原文档的 caution 提示):自 2024-08-16 起,出于性能考虑,获取文章列表的端点不再返回文章的 body 字段。受影响的端点为:

  • GET /api/articles
  • GET /api/articles/feed

这一契约变更在仓库中有两处可交叉验证的证据:

  1. OpenAPI 规范MultipleArticlesResponse 中,articles 数组内联定义的必填字段为 authorcreatedAtdescriptionfavoritedfavoritesCountslugtagListtitleupdatedAt——没有 body;而 SingleArticleResponse 引用的 Article schema 是包含 body 的。
  2. articles.hurl 对全部五种列表场景(无鉴权全局列表、按作者过滤、带鉴权列表、带鉴权按作者过滤、按 tag 过滤)都显式断言 jsonpath "$.articles[0].body" not exists,同时逐字段校验 title/slug/description 为字符串、tagList 为列表、时间戳匹配 ISO 8601 正则、favorited 为布尔、favoritesCount 为整数。

分页参数在 OpenAPI 的 parameters 定义 中有约束:offset 为不小于 0 的整数(跳过前 N 条),limit 为不小于 1 的整数、默认 20articlesCount 表示满足过滤条件的总条数,与当前页的 articles 长度无关。仓库中 pagination.hurllimit=1 配合 offset 逐页取回文章,验证了"最近优先 + 分页"的排序语义。

Comments:单条与多条评论响应对象

单条评论(创建 POST /api/articles/{slug}/comments 返回 201):

{
  "comment": {
    "id": 1,
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:22:56.637Z",
    "body": "It takes a Jacobian",
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }
}

多条评论(GET /api/articles/{slug}/comments,鉴权可选):

{
  "comments": [{
    "id": 1,
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:22:56.637Z",
    "body": "It takes a Jacobian",
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }]
}

OpenAPI Comment schema 必填字段为 authorbodycreatedAtidupdatedAt,其中 id 为整数(Hurl 断言 $.comment.id isInteger)。comments.hurl 的验证要点:

  • 创建评论后按 id 捕获变量,用于后续 DELETE /api/articles/{slug}/comments/{id}(204 无响应体)。
  • 列表断言 $.comments 为列表且元素级校验 id/body/时间戳格式/author.username,并区分了带鉴权与不带鉴权两种列表请求——两种情况下响应结构必须一致。

List of Tags:标签列表响应对象

GET /api/tags(无需鉴权)返回字符串数组,且 OpenAPI TagsResponse 要求顶层键 tags 必须存在:

{
  "tags": [
    "reactjs",
    "angularjs"
  ]
}

tags.hurl 的测试流程展示了该端点的典型用法:先注册用户并创建带标签的文章(tagList: ["h_xxx", "t_xxx"]),再断言 $.tags 为列表、长度不小于 1、包含刚创建的两个标签,且各元素为字符串。

错误响应:统一的多态错误信封

Error handling 文档 定义了与上述成功响应并列的失败响应格式——所有校验失败返回 422,其余状态码为 401(未提供鉴权)、403(无权操作)、404(资源不存在)。错误体统一为:

{
  "errors":{
    "body": [
      "can't be empty"
    ]
  }
}

即顶层 errors 对象的键是出错字段/资源名,值为字符串数组。OpenAPI 的 GenericErrorModel schema 与此一致(errors 为键到字符串数组的对象)。实际状态码对应的键名约定可从 OpenAPI 各错误响应示例与 Hurl 断言中读出:

状态码 场景 键名示例 依据
401 缺少有效 token token: ["is missing"] OpenAPI Unauthorized 响应示例
409 注册时用户名/邮箱已被占用 username: ["has already been taken"] OpenAPI ConflictError 示例
404 资源不存在 article: ["not found"]resource: ["not found"] OpenAPI NotFound 示例;articles.hurl 删除后断言 $.errors.article[0] == "not found"
422 字段校验失败 title: ["can't be blank"] OpenAPI GenericError 示例

错误键名在 404/403 场景下"标识资源类型"(article、comment、profile 等)这一点,直接写在 OpenAPI 对 NotFound/Forbidden 响应的 description 中,实现时请让键名与资源类型对应,方便前端和测试稳定解析。

用 Hurl 测试套件验证你的实现

仓库自带的 Hurl 集合覆盖了上述全部响应格式,是契约的可执行形态。运行方式(见 run-api-tests-hurl.sh):

# 安装 hurl 后,指向你的本地 API 运行全部用例
HOST=http://localhost:8000 ./specs/api/run-api-tests-hurl.sh

# 或只跑与响应格式直接相关的文件
HOST=http://localhost:8000 ./specs/api/run-api-tests-hurl.sh specs/api/hurl/auth.hurl specs/api/hurl/articles.hurl

脚本细节:默认 HOSThttp://localhost:8000;每轮运行自动生成 uid 变量(时间戳+进程号),使各用例注册互不冲突的用户;以 --test --jobs 1 串行执行,保证各文件内的前置请求(注册、建文章)先于断言发生。你也可以用 Bruno 集合 在 GUI 中逐条查看等效请求,两套工具由 hurl-to-bruno.js 转换保持同步。

对照检查清单(实现者视角):

  1. 所有成功响应是否为 application/json; charset=utf-8
  2. 顶层包装键(user/profile/article/articles/comment/comments/tags)是否拼写正确;
  3. bio/image 空值是否输出 null,且提交空字符串时是否被规范化为 null 并持久化;
  4. 时间戳是否符合 YYYY-MM-DDTHH:mm:ss(date-time)格式;
  5. GET /api/articlesGET /api/articles/feed 是否已剔除 body,且 articlesCount 始终返回;
  6. 列表接口是否支持 offset(>=0)/limit(>=1,默认 20);
  7. 错误响应是否为 422/401/403/404 + {"errors": {key: [messages]}} 信封。

只要这七条全部通过 Hurl 断言,你的后端就满足了 api-response-format.md 定义的完整响应契约,可以与 realworld 生态中的任何前端实现互通。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384