第 45 章 / 共 51 章
卷五 · 扩展、运维与实战

实战一:REST API 服务(todo 后端)

用一章的篇幅把前四卷串起来:一个带持久化、鉴权、分页、可热重载的 REST 服务——从空目录到验收通过。

本章导读

站位:三个实战的第一个,也是最通用的一个。建议真的动手做——每节末有验收点,全部通过即项目完成。参照物:demo-single 的 app.h/item.h 就是同款项目的成熟形态。

45.1 项目目标

做一个"待办事项"后端(任何资源同理):

能力接口用到的卷
资源 CRUDGET/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/reloadch26-27/38
部署形态单目录 + systemdch42

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 可用;请求日志带 IDch43
部署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 延伸阅读