| name | server-nodejs |
| description | Patterns and anti-patterns for the Node.js/Fastify server defined in spec 0007. Use when implementing or reviewing any file in servers/nodejs/. Trigger phrases: "nodejs server", "fastify", "better-sqlite3", "/server-nodejs".
|
| applyTo | ["servers/nodejs/**"] |
Server: Node.js 24 LTS + Fastify 5 + better-sqlite3 (spec 0007)
Run command
node index.js
Or via npm script:
{ "scripts": { "start": "node index.js" } }
Dependencies
{
"dependencies": {
"fastify": "5.8.5",
"better-sqlite3": "latest"
}
}
package-lock.json must be committed.
better-sqlite3 is synchronous by design
better-sqlite3 provides a synchronous API. This is intentional — SQLite is
I/O-bound, not compute-bound, and synchronous access removes async overhead.
const rows = db.prepare("SELECT * FROM items ORDER BY id").all();
const rows = await db.prepare("SELECT * FROM items ORDER BY id").all();
Do not wrap better-sqlite3 calls in async functions expecting promises.
The synchronous calls are fast enough that Node.js's event loop handles them fine
for this workload.
Fastify 5 — basic setup
import Fastify from 'fastify';
const fastify = Fastify({ logger: false });
const port = parseInt(process.env.PORT ?? '8080', 10);
const host = '0.0.0.0';
await fastify.listen({ port, host });
Fastify 5 — route registration with JSON schema
Fastify uses JSON schemas for fast serialization (via fast-json-stringify).
Always register schemas on routes that return data:
const itemSchema = {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
created_at: { type: 'string' },
},
required: ['id', 'name', 'created_at'],
};
fastify.get('/items', {
schema: {
response: { 200: { type: 'array', items: itemSchema } },
},
}, async (request, reply) => {
return db.prepare('SELECT id, name, created_at FROM items ORDER BY id').all();
});
Without the schema, Fastify falls back to JSON.stringify (slower) and skips
validation and serialization optimizations.
Status codes
fastify.post('/items', {
schema: { response: { 201: itemSchema, 400: errorSchema } },
}, async (request, reply) => {
const { name } = request.body ?? {};
if (!name || name.trim() === '') {
return reply.code(400).send({ error: 'name is required' });
}
const item = insertItem(name);
return reply.code(201).send(item);
});
fastify.delete('/items/:id', async (request, reply) => {
const deleted = deleteItem(parseInt(request.params.id, 10));
if (!deleted) return reply.code(404).send({ error: 'not found' });
return reply.code(204).send();
});
.send() must always be called, even for 204. Without .send(), the response
hangs until Fastify times out.
created_at — UTC with Z suffix
const created_at = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
new Date().toISOString()
Database setup
import Database from 'better-sqlite3';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dbPath = process.env.DB_PATH
?? path.join(__dirname, '../../data/benchmark.db');
const db = new Database(dbPath, { readonly: false });
db.pragma('journal_mode = WAL');
Single-process constraint (RF6)
import cluster from 'cluster';
cluster.fork();
Path parameters
fastify.get('/items/:id', async (request, reply) => {
const id = parseInt(request.params.id, 10);
if (isNaN(id)) return reply.code(404).send({ error: 'not found' });
});
Error response shape
Fastify's default error serializer produces {"statusCode":..., "error":..., "message":...}.
Always return explicit objects to match the spec:
reply.code(404).send({ error: 'not found' });
throw new Error('not found');
Anti-patterns
| Anti-pattern | Problem | Fix |
|---|
await db.prepare(...).all() | better-sqlite3 is sync, no promise returned | Remove await |
.send() missing on 204 | Response hangs | Always call .send() |
new Date().toISOString() as-is | Includes milliseconds .000Z | Strip with .replace(/\.\d{3}Z$/, 'Z') |
| No JSON schema on route | Skips fast-json-stringify optimization | Always add schema.response |
throw new Error(...) for 4xx | Produces wrong error shape | Return reply.code(4xx).send({error: '...'}) |
cluster.fork() | Violates single-process constraint | Delete it |