第 45 章 / 共 51 章
卷五 · 扩展、运维与实战
实战一:REST API 服务(todo 后端)
用一章的篇幅把前四卷串起来:一个带持久化、鉴权、分页、可热重载的 REST 服务——从空目录到验收通过。
本章导读
站位:三个实战的第一个,也是最通用的一个。建议真的动手做——每节末有验收点,全部通过即项目完成。参照物:demo-single 的 app.h/item.h 就是同款项目的成熟形态。
45.1 项目目标
做一个"待办事项"后端(任何资源同理):
| 能力 | 接口 | 用到的卷 |
|---|---|---|
| 资源 CRUD | GET/POST /api/v1/todos、GET/PUT/PATCH/DELETE /api/v1/todos/{id} | 卷三(14-18) |
| 持久化 | SQLite,换代数据无损 | ch32 + ch28 |
| 分页与过滤 | ?page=&size=&done= | ch18 |
| 令牌鉴权 | X-Api-Token 头 | ch27/39 |
| 运维面 | /health、/stats、/admin/reload | ch26-27/38 |
| 部署形态 | 单目录 + systemd | ch42 |
45.2 地基(30 分钟)
- 骨架:按 ch05 约定建目录(modules/ route_http/ db/ wwwroot/);main.c 编排 include;xs.json 按 ch07/08 写最小配置
- 模块三件:modules/http.h(ch15 的 Reply 族)、modules/db.h(ch32 的 DB_Init/Unit)、modules/route.h(ch17 双表)
- 建表:
todo(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER DEFAULT 0, created INTEGER)
验收点 1:服务启动、日志五段齐全(ch02 字典)、/health 返回 ok。
45.3 CRUD 与错误约定
创建与更新(核心两段)
/* POST /api/v1/todos:读 JSON(ch16+ch18)→ 绑定插入(ch32)*/
static void Handle_Create(XS_HttpReq* pReq, const RouteParamHTTP* a, uint32 n)
{
xvalue* pBody = ReadRequestJSON(pReq); /* 封装:body→parse */
if ( pBody == NULL ) return (void)ApiError(pReq, 400, "bad_json");
xvalue* pTitle = xrtValueObjectGet(pBody, "title");
if ( pTitle == NULL || !IsNonEmptyString(pTitle) )
return (void)(xrtValueRelease(pBody),
ApiError(pReq, 422, "field_required:title"));
...
int64 iId = DB_InsertTodo(ViewOf(pTitle));
xrtValueRelease(pBody);
ReplyItemJSON(pReq, 201, iId); /* 201 + 新资源 */
}
/* PUT/PATCH /api/v1/todos/{id}:路径参数 + 部分更新 */
/* 404(不存在)/ 422(字段错)/ 200(成功)三分错误约定 */
错误约定先行(做 API 的关键习惯):400 畸形 JSON、401 令牌、404 资源、405 方法(路由自动)、413 超限(驱动自动)、422 语义校验——每类都有机器可读的 code 字段。资源不存在时 PUT 用 404 还是 upsert?选定并写进文档,别两种都出现。
验收点 2:七个 CRUD 接口 curl 全通;错误三分约定在三种故意输入下行为正确。
45.4 分页与鉴权
- 分页:列表接口
SELECT ... LIMIT ? OFFSET ?+ 总数SELECT COUNT(*);响应体{"total":N,"page":P,"data":[...]}——上限 clamp(如 size ≤ 100)是必须的防线 - 鉴权中间层:模块层一个
CheckToken(pReq)(常数时间比较),全部 /api/* 路由处理器第一行调用;令牌进 Custom 旋钮(ch09 的严格读取) - 热重载演练:改列表排序规则 → reload → 验证(a)在途请求无错(b)数据无损(ch32 代际)(c)计数器跨代连续(Swap 接一个 total 计数——ch28 的毕业检查)
验收点 3:压测中换代 3 次零失败;total 计数连续;令牌错误 401 且限速生效。
45.5 验收清单
| 维度 | 标准 | 章节依据 |
|---|---|---|
| 功能 | CRUD/分页/过滤/鉴权全通 | 本章 |
| 协议 | HEAD 支持(GET|HEAD 注册)、405+Allow 正确 | ch15/17 |
| 持久 | 重启与换代数据无损 | ch32 |
| 韧性 | 压测换代零失败;限额四态正确 | ch28/09 |
| 观测 | /health /stats 可用;请求日志带 ID | ch43 |
| 部署 | systemd 单元 + 优雅停机窗口 | ch42 |
常见错误
先写接口后定错误约定
- 症状
- 后期统一错误格式要改所有 handler。
- 原因
- 错误约定是 API 的宪法——45.3 第一步就定死(ApiError 辅助函数 + code 表)。
size 参数没 clamp
- 症状
- ?size=1000000 一发,服务开始深分页扫描。
- 原因
- 分页参数是客户端输入——上限 clamp 与默认值是接口的一部分。
编程练习
基础完成项目
按 45.2-45.5 完成全部验收点。
验收:六维验收清单全绿。
进阶并发编辑语义
加 updated 字段与 If-Match 乐观锁(409 冲突)。
验收:两个并发 PUT 只有一个成功。
挑战OpenAPI 文档路由
实现 /api/openapi.json:用 xvalue 生成接口描述(路由表驱动的文档生成)。
验收:文档与实现零漂移(同源生成)。
小结
| 要点 | 结论 |
|---|---|
| 流程 | 骨架→模块三件→错误约定→CRUD→分页鉴权→验收 |
| 关键习惯 | 错误约定先行;参数 clamp;鉴权统一入口 |
| 毕业标志 | 压测换代零失败 + 数据无损 + 计数连续 |
本章涉及的 API 参考
xrt 延伸阅读
- xrt · value 与 json —— 本项目的主力工具