feat(F-004): typed fail-fast config and feature flag module

- loadConfig: pure over env object, accumulates all problems, names var names only
- DATABASE_URL now required at startup; PORT/HOST/LOG_LEVEL/NODE_ENV/REDIS_URL defaulted
- flags module behind FeatureFlagProvider; unknown flags OFF; runtime setEnabled (no redeploy)
- buildApp decorates app.flags; server.ts fail-fast before app boot
- tests caught and fixed flag-store case-normalization bug before gates
- zero new dependencies; all gates approved; verify.sh green
This commit is contained in:
rikrdo
2026-08-14 22:29:18 +02:00
parent 41f144d7bd
commit 4851692031
25 changed files with 756 additions and 18 deletions

View File

@@ -8,15 +8,24 @@ import Fastify, { type FastifyInstance } from 'fastify';
import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
import type { IncomingMessage } from 'node:http';
import { registerHealthRoutes } from '../modules/health/index.js';
import { createFlagStore, type FeatureFlagProvider } from '../modules/flags/index.js';
import { AppError, errorEnvelope } from '../shared/errors.js';
import { createLogger, type Logger } from '../infrastructure/logging/logger.js';
declare module 'fastify' {
interface FastifyInstance {
flags: FeatureFlagProvider;
}
}
const REQUEST_ID_HEADER = 'x-request-id';
const SAFE_REQUEST_ID = /^[A-Za-z0-9._-]{1,128}$/;
export interface BuildAppDeps {
/** Injectable logger so tests can capture structured output. */
logger?: Logger;
/** Feature flags. Default: empty store, every flag OFF (fail-safe). */
flags?: FeatureFlagProvider;
}
function generateRequestId(raw: IncomingMessage): string {
@@ -33,9 +42,11 @@ function generateRequestId(raw: IncomingMessage): string {
*/
export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance> {
const logger = deps.logger ?? createLogger();
const flags = deps.flags ?? createFlagStore();
const startTimes = new WeakMap<FastifyRequest, number>();
const app = Fastify({ logger: false, genReqId: generateRequestId });
app.decorate('flags', flags);
app.addHook('onRequest', async (request, reply) => {
startTimes.set(request, performance.now());