---
title: 从 ChatCrystal 学 Fastify：插件、校验与失败路径
description: 对照真实 createServer 与路由边界，在隔离练习中运行 Fastify inject，验证成功、无效输入和不存在的路由。
image: "https://zengliangyi.com/og/blog/learn-fastify-with-chatcrystal.png"
---

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

## 从真实入口理解组织方式

固定 [createServer](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/index.ts) 创建 Fastify、注册 CORS、初始化数据库与自动保存，再注册 health、setup、status、import、conversations、notes、config、relations 和 memory 路由。云端认证钩子在受保护路由之前注册；本地扫描与云端导入还有各自的业务检查。不能把所有路由概括成无需认证的公开 API。

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

## 在独立目录运行一个练习

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



```bash
npm   init   -y  
  npm   install   fastify@5.12.3  
  node   lesson.mjs
```





```javascript
// 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 中保证结束清理，见 [官方测试指南](https://fastify.dev/docs/latest/Guides/Testing/)。练习实际检查 200、400 与 404，还应故意删掉 body schema：同一份无效输入断言必须失败；恢复后再通过。这样确认检查真的覆盖输入拒绝，而不是只看成功请求。

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

练习使用完整 JSON Schema 验证请求体，参见 [Fastify 校验文档](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/)。项目的 [会话查询路由](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/routes/conversations.ts) 则使用类型断言、条件拼接和 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。这个顺序本身不能证明所有在途数据库请求已排空；不要将它写成无数据丢失保证。测试关闭行为应另设未完成请求与资源替身。导入、保存和进程范围见 [内存预算](/blog/conversation-import-memory-budget)。

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

分享：

##  相关文章

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

核对 ChatCrystal 的实际构建与静态目录探测，用隔离 inject 练习验证页面、未知 API 和缺失资源的不同响应。          曾亮懿       2026年10月5日      阅读约 4 分钟

#ChatCrystal

#Fastify

#Node.js](/blog/fastify-production-static-service)    [##  AI 对话格式差异：事件、快照与跨库消息定位

对照五个固定适配器，区分 JSONL 容器、事件语义与 SQLite 键布局，用两个内存数据库观察消息索引、孤立数据和未知版本。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-format-divergence)    [##  AI 对话管理方案：四种存储路径与迁移验收

比较 Claude Code、Cursor、Copilot、Trae 的固定适配器输入，区分原始副本、可读导出与知识库，并用合成清单验证身份和信息保留。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-management-options)

##  从文章到代码

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

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "从 ChatCrystal 学 Fastify：插件、校验与失败路径",
  "description": "对照真实 createServer 与路由边界，在隔离练习中运行 Fastify inject，验证成功、无效输入和不存在的路由。",
  "url": "https://zengliangyi.com/blog/learn-fastify-with-chatcrystal/",
  "image": "https://zengliangyi.com/images/blog/learn-fastify-with-chatcrystal.svg",
  "datePublished": "2026-10-04T00:00:00.000Z",
  "dateModified": "2026-10-04T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/learn-fastify-with-chatcrystal/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "从 ChatCrystal 学 Fastify：插件、校验与失败路径",
      "item": "https://zengliangyi.com/blog/learn-fastify-with-chatcrystal/"
    }
  ]
}
```
