RealWorld 后端实现指南:以 OpenAPI 与 Hurl 测试套件定义的 API 契约
RealWorld 是“the mother of all demo apps”——同一套 Medium 风格应用契约被 React、Angular、Node、Django 等众多技术栈共同实现,其前提是所有后端都严格遵循同一份 API 规范。本文基于仓库中的后端规范入口文档 introduction.md 展开,讲清契约的三个载体(OpenAPI 规范、Hurl 测试套件、Bruno 集合)、本地运行测试的完整命令与参数,以及“测试即唯一事实来源”这一核心原则,读完即可直接启动后端开发并用自带测试套件验收你的实现。
一、契约优先:所有后端实现必须遵循同一份 API 规范
规范文档开宗明义:所有后端实现都必须遵循 specs/api 目录下的 API 规范。完整的接口定义以机读形式存放在 openapi.yml,这是唯一描述全部接口行为的 OpenAPI 规范文件。
该入口文档特别用提示框强调了后端规范中最重要的原则:
测试是事实来源(The tests are the source of truth)。 文档页面只是用自然语言对契约的总结,真正定义契约的是 OpenAPI 规范 和 Hurl 测试套件。当文字描述与测试不一致时,以测试为准——对着测试来开发,发现不一致时请提交 issue。
这条原则对多语言、多框架的 RealWorld 生态至关重要:几十个技术栈的后端要实现互操作,散文式的描述允许歧义,而可执行的请求-断言序列不会。因此规范文档的定位是“快速索引和可读摘要”,可执行测试才是验收标准。
二、API 契约覆盖的接口面
配合 OpenAPI 规范与 endpoints.md 文档,当前契约覆盖的接口可以归为五组(所有路由均以 /api 为前缀,认证方式为请求头 Authorization: Token jwt.token.here):
| 分组 | 接口 | 认证要求 |
|---|---|---|
| 认证 | POST /api/users/login、POST /api/users、GET /api/user、PUT /api/user |
前两者免认证,后两者必须认证 |
| 资料与关注 | GET /api/profiles/:username、POST /api/profiles/:username/follow、DELETE /api/profiles/:username/follow |
获取资料可选认证,关注/取关必须认证 |
| 文章 | GET /api/articles(支持 tag/author/favorited/limit/offset 查询参数)、GET /api/articles/feed、GET /api/articles/:slug、POST /api/articles、PUT /api/articles/:slug、DELETE /api/articles/:slug |
列表与详情免认证,写操作必须认证 |
| 评论 | POST /api/articles/:slug/comments、GET /api/articles/:slug/comments、DELETE /api/articles/:slug/comments/:id |
创建与删除必须认证,列表可选 |
| 收藏与标签 | POST /api/articles/:slug/favorite、DELETE /api/articles/:slug/favorite、GET /api/tags |
收藏操作必须认证,标签免认证 |
几个在实现时容易忽略、但测试会严格校验的细节:
- 列表分页参数:
GET /api/articles的limit默认 20、offset默认 0,结果按最新在前排序;GET /api/articles/feed同样接受limit/offset,但只返回已关注用户创建的文章。 - slug 更新语义:
PUT /api/articles/:slug修改title时 slug 必须随之更新;规范只要求 slug 是可用于取/改/删的唯一字符串,重复标题也必须生成不同 slug,格式本身不强制(常见做法是 kebab-case)。 - 空字符串归一化:用户资料的
bio、image属于可空字段,提交空字符串应被归一化为null并持久化——这一行为在 auth.hurl 中有专门的用例覆盖(如“Update user bio to empty string - should normalize to null”)。
三、用 Hurl 测试套件验证你的后端
入口文档给出的核心便利设施是 specs/api/hurl 目录下的 Hurl 测试集合:在你实现 API 端点的过程中随时跑一遍即可对照契约。全套测试由脚本 run-api-tests-hurl.sh 一键执行:
HOST=http://localhost:3000/api ./run-api-tests-hurl.sh
specs/api 的 README 中也给出了完全一致的命令(把 HOST 指向你的后端根路径即可)。阅读脚本源码可以确认其参数语义:
HOST环境变量:被测后端地址,默认值为http://localhost:8000(脚本内HOST="${HOST:-http://localhost:8000}");UID_VAL环境变量:默认取date +%s拼接进程号,作为测试数据的唯一后缀,保证多次运行不产生用户名/邮箱冲突;- 底层执行
hurl --test --jobs 1,串行执行以维持请求间的依赖顺序,并把host、uid注入为 Hurl 变量; - 位置参数可用于只跑部分文件:不带参数时执行
hurl/下全部.hurl文件,带参数则只跑指定文件。
测试文件按功能域划分,覆盖正常流与错误流两类场景:
- 正常流:auth.hurl、articles.hurl、comments.hurl、favorites.hurl、feed.hurl、pagination.hurl、profiles.hurl、tags.hurl;
- 错误流:errors_auth.hurl、errors_articles.hurl、errors_authorization.hurl、errors_comments.hurl、errors_profiles.hurl。
以 auth.hurl 中的注册请求为例,可以看到 Hurl 用例的完整写法——请求、预期状态码、jsonpath 断言与变量捕获:
# Register
POST {{host}}/api/users
{
"user": {
"username": "auth_{{uid}}",
"email": "auth_{{uid}}@test.com",
"password": "password123"
}
}
HTTP 201
[Asserts]
jsonpath "$.user.username" == "auth_{{uid}}"
jsonpath "$.user.email" == "auth_{{uid}}@test.com"
jsonpath "$.user.bio" == null
jsonpath "$.user.image" == null
jsonpath "$.user.token" isString
jsonpath "$.user.token" not isEmpty
[Captures]
reg_token: jsonpath "$.user.token"
这段用例同时验证了三个契约点:注册返回 201 而非 200;新用户 bio/image 必须为 null;token 必须是非空字符串。捕获的 token 会供后续用例(如 Authorization: Token {{token}})复用,整条链路因此能自动验证“注册→登录→取当前用户→更新资料→验证持久化”的完整生命周期。
四、Bruno 集合:与 Hurl 同步的 GUI 工作流
如果偏好图形化调试,specs/api/bruno 目录下提供了 Bruno 请求集合,其结构(auth/、articles/、errors-* 等文件夹及编号命名的 .bru 文件)与 Hurl 用例一一对应,可直接用 Bruno 应用打开交互式执行。它的执行脚本为 run-api-tests-bruno.sh:
HOST=http://localhost:3000/api ./run-api-tests-bruno.sh
脚本逐文件夹调用 bun x @usebruno/cli run,通过 --env local 读取 environments/local.bru 中的 host 变量(默认 http://localhost:3000),并支持以位置参数指定子集、BRUNO_SANDBOX 环境变量(默认 safe)控制沙箱级别。
从源码结构看,Bruno 集合不是手工维护的第二套测试:它由 hurl-to-bruno.js 从 Hurl 文件解析生成(按注释行、方法行、请求体分段解析后写出 .bru 文件),仓库 Makefile 提供 make bruno-generate 生成、make bruno-check 在 CI 中校验同步。specs/api/README.md 明确指出:Hurl 文件是事实来源,Bruno 集合是生成物——这与入口文档“测试优先于散文”的原则一脉相承。
五、契约的其他关键部分:响应格式与错误处理
入口文档在结尾引导读者继续阅读后端规范的其余三页,这里给出与测试用例相互印证的核心要点,便于实现时一次性对齐:
响应格式(详见 api-response-format.md):所有对象包裹在单数键下,如 {"user": {...}}、{"article": {...}}、{"profile": {...}}、{"tags": [...]},列表接口额外返回 articlesCount。需要特别注意的是,自 2024-08-16 起,GET /api/articles 与 GET /api/articles/feed 不再返回文章的 body 字段(出于性能考虑),文章正文只能通过 GET /api/articles/:slug 获取——这是测试与文档共同约束的行为。
错误处理(详见 error-handling.md):校验失败返回 422,错误体格式为:
{
"errors": {
"body": ["can't be empty"]
}
}
此外契约约定:401 表示缺少认证、403 表示无权操作(例如 B 用户删除 A 用户的文章)、404 表示资源不存在。这些状态码分别由 errors-auth、errors-authorization、errors-articles 等错误流用例逐一验证。
六、小结:对着测试构建,按规范索引深入
RealWorld 后端规范的阅读路径可以概括为三步:
- 以 OpenAPI 规范 为接口全集,快速索引见 endpoints.md、api-response-format.md 与 error-handling.md;
- 开发过程中随时运行
HOST=<你的地址> ./run-api-tests-hurl.sh(或 Bruno 等价流程)做回归验证,Hurl 文件位于 specs/api/hurl,运行细节见 hurl.md 与 bruno.md; - 文档与测试不一致时以测试为准,并把差异作为 issue 反馈。
遵循这条路径,你的后端实现就与 RealWorld 生态中其他技术栈的实现处于同一契约之下。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00