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 服务逐步阅读;每新增一种行为,都同时检查可接受输入和会失败的输入。