# DESIGN — F-003 HTTP foundation and request context ## Architecture All cross-cutting HTTP behavior lives in explicit, readable code: - `src/infrastructure/logging/logger.ts` — pino factory. JSON only, level from `LOG_LEVEL` (default `info`), base field `service`. Accepts an optional writable destination so tests can capture output without transports (no magic). - `src/shared/errors.ts` — `AppError` class (statusCode, code, message) and the single `errorEnvelope(statusCode, code, message, requestId)` builder. Envelope v2: `{ error: { statusCode, code, message }, requestId }`. - `src/shared/http-input.ts` — `parseJson(schema, input)` using zod. On failure throws `AppError(400, VALIDATION_ERROR)` carrying field-level issues (field paths only, never internal state). - `src/app/build-app.ts` — composition root wires everything explicitly: - Fastify `genReqId`: trust incoming `x-request-id` only if it matches `^[A-Za-z0-9._-]{1,128}$`; otherwise generate `crypto.randomUUID()`. - `onRequest` hook: set `x-request-id` response header, start timer. - `onResponse` hook: one JSON log line per request `{ requestId, method, url, statusCode, durationMs }`. - `setErrorHandler`: map `AppError` and Fastify errors to the envelope. 4xx expose their message; >=500 always returns generic `Internal Server Error`. Errors are logged with stack server-side, tagged with requestId. - `setNotFoundHandler`: envelope with code `NOT_FOUND` + requestId. - `src/infrastructure/http/server.ts` — startup log lines through the logger. ## Request id policy - Propagation is opt-in and sanitized; anything suspicious is replaced by a fresh UUID. - Response header name: `x-request-id`. ## Validation policy - Validation is explicit per route: call `parseJson(schema, body)` inside the handler. - No schema decorators, no magic body binding. The F-003 deliverable is the hook (helper + envelope integration), exercised by composition tests with a test-only route registered on the built app. ## Backward compatibility - `GET /health` response body unchanged; envelope gains fields (additive). ## Files | File | Role | |---|---| | src/infrastructure/logging/logger.ts | pino factory | | src/shared/errors.ts | AppError + envelope v2 | | src/shared/http-input.ts | zod parse helper | | src/app/build-app.ts | hooks, genReqId, handlers | | src/infrastructure/http/server.ts | startup logging | | src/app/tests/http-foundation.test.ts | composition tests (request id, logs, envelope, no leak) | | src/shared/tests/http-input.test.ts | helper unit tests | ## Risks / mitigations - Double logging (fastify internal + hooks): keep `logger: false` on Fastify, log only via explicit hooks. - Header injection through propagated id: strict regex + length cap. - 5xx leaking internals: handler returns fixed generic message for anything >=500; tests assert absence of thrown message/stack.