import express, { type ErrorRequestHandler } from "express"; import { randomUUID } from "node:crypto"; import { pinoHttp } from "pino-http"; import type { Logger } from "pino"; import type { Config } from "./lib/config.js"; import type { Db } from "./lib/db.js"; import type { Cache } from "./lib/redis.js"; import { healthRouter } from "./routes/health.js"; import { logDemoRouter } from "./routes/log-demo.js"; import { rootRouter } from "./routes/root.js"; export interface Deps { config: Config; logger: Logger; db?: Db; cache?: Cache; } export function createApp({ config, logger, db, cache }: Deps) { const app = express(); app.disable("x-powered-by"); app.use( pinoHttp({ logger, genReqId: (req, res) => { const incoming = req.headers["x-request-id"]; const id = (Array.isArray(incoming) ? incoming[0] : incoming) ?? randomUUID(); res.setHeader("X-Request-Id", id); return id; }, // Keep the logging contract flat and small: req.id/method/url, // res.statusCode, responseTime. serializers: { req: (req) => ({ id: req.id, method: req.method, url: req.url }), res: (res) => ({ statusCode: res.statusCode }), }, autoLogging: { ignore: (req) => req.url === "/healthz" || req.url === "/readyz", }, }), ); app.use(healthRouter({ db, cache })); app.use(rootRouter({ config, db, cache })); app.use(logDemoRouter()); // Express identifies error handlers by arity; keep all four args. // eslint-disable-next-line @typescript-eslint/no-unused-vars const onError: ErrorRequestHandler = (err, req, res, _next) => { req.log.error({ err }, "unhandled error"); res.status(500).json({ error: "internal error" }); }; app.use(onError); return app; }