跳到正文
曾亮懿 ZengLiangYi Blog

从 ChatCrystal 学 Fastify:插件、校验与失败路径

对照真实 createServer 与路由边界,在隔离练习中运行 Fastify inject,验证成功、无效输入和不存在的路由。

曾亮懿

曾亮懿

阅读约 3 分钟
目录 展开 收起

ChatCrystal 的后端可作为 Fastify 阅读材料,但学习框架能力时要分清“框架支持”与“项目实际使用”。本文不提供 Express 性能排名,也不把简化练习称为应用源码。

从真实入口理解组织方式

固定 createServer 创建 Fastify、注册 CORS、初始化数据库与自动保存,再注册 health、setup、status、import、conversations、notes、config、relations 和 memory 路由。云端认证钩子在受保护路由之前注册;本地扫描与云端导入还有各自的业务检查。不能把所有路由概括成无需认证的公开 API。

路由文件导出接收 FastifyInstance 的函数,并在函数内注册 URL。默认 封装规则 让子上下文继承祖先的装饰器和钩子,父级与兄弟不会自动获得子级新增项;这不意味着插件里的 URL 无法被 HTTP 访问。模块导入顺序和插件可见性仍需显式设计。

在独立目录运行一个练习

使用 Node 24 以上,在空目录安装与固定应用依赖一致的版本,保存下列代码为 lesson.mjs:

npm init -y
npm install fastify@5.12.3
node lesson.mjs
// lesson.mjs: an isolated exercise, not a ChatCrystal route.
import assert from 'node:assert/strict';
import Fastify from 'fastify';

const app = Fastify({ logger: false });
app.register(async function lessonRoutes(scope) {
  scope.get('/api/hello', async () => ({ message: 'Hello Fastify' }));
  scope.post(
    '/api/lesson',
    {
      schema: {
        body: {
          type: 'object',
          required: ['title'],
          additionalProperties: false,
          properties: { title: { type: 'string', minLength: 1 } },
        },
      },
    },
    async (request) => ({ title: request.body.title })
  );
});

try {
  const hello = await app.inject({ method: 'GET', url: '/api/hello' });
  assert.equal(hello.statusCode, 200);
  assert.equal(hello.json().message, 'Hello Fastify');
  const invalid = await app.inject({ method: 'POST', url: '/api/lesson', payload: {} });
  assert.equal(invalid.statusCode, 400);
  const valid = await app.inject({
    method: 'POST',
    url: '/api/lesson',
    payload: { title: 'Plugin exercise' },
  });
  assert.equal(valid.statusCode, 200);
  assert.equal(valid.json().title, 'Plugin exercise');
  const absent = await app.inject({ method: 'GET', url: '/api/absent' });
  assert.equal(absent.statusCode, 404);
  console.log('hello, invalid input, valid input, and missing route checked');
} finally {
  await app.close();
}

inject() 等待插件启动并模拟请求,不必监听端口;close() 放在 finally 中保证结束清理,见 官方测试指南。练习实际检查 200、400 与 404,还应故意删掉 body schema:同一份无效输入断言必须失败;恢复后再通过。这样确认检查真的覆盖输入拒绝,而不是只看成功请求。

类型、运行时校验与事实各有边界

练习使用完整 JSON Schema 验证请求体,参见 Fastify 校验文档。项目的 会话查询路由 则使用类型断言、条件拼接和 SQL 参数;TypeScript 的 as 不会在运行时检查用户输入。其他业务入口还会用 Zod 与显式判断,所以不能声称整个项目没有运行时校验。

阅读具体路由时依次追踪参数来源、默认值、转换、失败返回、SQL 占位参数和响应字段。参数化 SQL 保护值的位置,不会自动验证 limit、offset 的业务范围;schema 通过也不表示请求所述事实正确。

静态文件与关闭顺序要照实现读

应用只在找到 client/dist 时注册静态服务与 SPA fallback。本地或已通过云端访问检查的未知 /api/ 请求返回 JSON 404;云端尚未完成设置或未认证时,会先得到 403 或 401。其他未知路径可由 fallback 返回前端入口。CORS 的 origin: true 在当前注册中没有按生产关闭,不能把同源部署当成源码已经收紧跨域策略。

当前 shutdown 先关闭 watcher、调用 closeDatabase 保存并关闭数据库,再调用 app.close。这个顺序本身不能证明所有在途数据库请求已排空;不要将它写成无数据丢失保证。测试关闭行为应另设未完成请求与资源替身。导入、保存和进程范围见 内存预算。

完成练习后,再沿真实 health 路由到 notes 服务逐步阅读;每新增一种行为,都同时检查可接受输入和会失败的输入。

分享:

相关文章

Fastify 生产静态服务:同端口、SPA fallback 与失败路径

核对 ChatCrystal 的实际构建与静态目录探测,用隔离 inject 练习验证页面、未知 API 和缺失资源的不同响应。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Fastify #Node.js

AI 对话格式差异:事件、快照与跨库消息定位

对照五个固定适配器,区分 JSONL 容器、事件语义与 SQLite 键布局,用两个内存数据库观察消息索引、孤立数据和未知版本。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #SQLite

AI 对话管理方案:四种存储路径与迁移验收

比较 Claude Code、Cursor、Copilot、Trae 的固定适配器输入,区分原始副本、可读导出与知识库,并用合成清单验证身份和信息保留。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #SQLite

从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录,欢迎交流实现细节。