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.