Skip to content
曾亮懿 ZengLiangYi Blog

Learn Fastify from ChatCrystal: plugins, validation, and failures

Read the actual createServer and route boundaries, then use an isolated inject exercise to check success, invalid input, and missing routes.

曾亮懿

曾亮懿

3 min read
Table of contents Show Hide

ChatCrystal’s backend is useful Fastify reading material when framework capabilities are distinguished from project behavior. This article has no Express performance ranking and does not present a simplified exercise as application source.

Read the actual server composition

The fixed createServer creates Fastify, registers CORS, initializes the database and autosaving, and registers health, setup, status, import, conversations, notes, config, relations, and memory routes. Cloud authentication is registered before protected routes. Local scans and cloud ingestion also have business checks; these routes are not all public unauthenticated APIs.

Route modules export functions receiving FastifyInstance and registering URLs. Default encapsulation lets children inherit ancestor decorators and hooks; parents and siblings do not automatically receive additions inside a child. It does not hide the plugin’s URLs from HTTP clients. Import order and plugin visibility still require deliberate design.

Run an isolated exercise

Use Node 24 or newer in an empty directory, install the version used by the fixed application’s dependency declaration, and save the following as 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() waits for plugin startup and simulates requests without listening on a port. Putting close() in finally ensures cleanup; see the official testing guide. The exercise checks 200, 400, and 404. Deliberately remove the body schema: the same invalid-input assertion must fail, then pass after restoration. This verifies rejection rather than only a successful request.

Separate types, runtime validation, and facts

The exercise validates a body using full JSON Schema; see Fastify validation. The project’s conversation route instead uses type assertions, conditions, and SQL parameters. TypeScript as does not validate runtime input. Other business entry points use Zod and explicit checks, so the project is not devoid of runtime validation.

Trace each route’s input source, defaults, conversion, failure response, SQL placeholders, and output fields. Parameterized SQL protects value positions but does not automatically validate the business range of limit and offset. Schema acceptance is also not proof that a request’s factual claims are correct.

Read static serving and shutdown literally

The application registers static serving and an SPA fallback only when client/dist exists. Unknown /api/ paths return JSON 404 locally or after cloud access checks succeed. An incomplete cloud setup or missing authentication produces 403 or 401 first. Other unknown paths can return the frontend entry through the fallback. Its CORS registration uses origin: true without disabling it in production; same-origin deployment does not establish a stricter source policy.

Shutdown closes the watcher, calls closeDatabase to save and close the database, then calls app.close. That order alone does not prove all in-flight database requests have drained or establish a no-data-loss guarantee. A shutdown exercise needs unfinished requests and resource substitutes. See memory budgets for import, persistence, and process boundaries.

After this exercise, read from the real health route into note services. For every behavior, check acceptable inputs and inputs that must fail.

Share:

Related Posts

Fastify production files: one port, SPA fallback, and failures

Check ChatCrystal’s build outputs and static-root discovery, then use isolated inject exercises to distinguish pages, unknown APIs, and missing assets.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Fastify #Node.js

AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Node.js #SQLite

How Claude Code, Cursor, Copilot, and Trae Store Chats on Disk

Compare Claude Code, Cursor, Copilot, and Trae storage paths. Distinguish raw copies from exports, and verify migration retention with test manifests.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Node.js #SQLite

From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.