diff --git a/src/http/server.ts b/src/http/server.ts index 251064b..09ac4b5 100644 --- a/src/http/server.ts +++ b/src/http/server.ts @@ -1,4 +1,4 @@ -import http from 'node:http'; +import http, { type IncomingMessage, type ServerResponse } from 'node:http'; import type { DatabaseSync } from 'node:sqlite'; import { systemClock, type Clock } from '../domain/clock.ts'; import { consoleNotifier, type Notifier } from '../notifications/notifier.ts'; @@ -36,27 +36,13 @@ export function buildRouter(deps: AppDependencies): Router { return router; } -export function createServer(options: ServerOptions): http.Server { - const deps: AppDependencies = { - service: new BookingService(options.db), - clock: options.clock ?? systemClock, - notifier: options.notifier ?? consoleNotifier, - }; - const log = options.log ?? console.log; - const router = buildRouter(deps); +type RawHandler = (req: IncomingMessage, res: ServerResponse) => Promise; - return http.createServer(async (req, res) => { - const url = new URL(req.url ?? '/', 'http://localhost'); +// Convierte cualquier error en una respuesta JSON. +function withErrors(next: RawHandler): RawHandler { + return async (req, res) => { try { - if (req.method === 'GET' && (await serveStatic(url.pathname, res))) return; - const match = router.match(req.method ?? 'GET', url.pathname); - if (match.kind === 'not-found') { - throw new HttpError(404, 'NOT_FOUND', `No existe la ruta ${url.pathname}.`); - } - if (match.kind === 'method-not-allowed') { - throw new HttpError(405, 'METHOD_NOT_ALLOWED', `${req.method} no está permitido en ${url.pathname}.`); - } - await match.handler({ req, res, url, params: match.params }); + await next(req, res); } catch (error) { if (error instanceof HttpError) { sendError(res, error); @@ -64,8 +50,45 @@ export function createServer(options: ServerOptions): http.Server { console.error(error); sendError(res, new HttpError(500, 'INTERNAL_ERROR', 'Ocurrió un error inesperado.')); } + } + }; +} + +// Escribe una línea de log al terminar, pase lo que pase. Va por fuera de +// withErrors para ver el código de estado que de verdad se respondió. +function withLog(log: (line: string) => void, next: RawHandler): RawHandler { + return async (req, res) => { + try { + await next(req, res); } finally { log(`${req.method} ${req.url} ${res.statusCode}`); } - }); + }; +} + +function dispatcher(router: Router): RawHandler { + return async (req, res) => { + const url = new URL(req.url ?? '/', 'http://localhost'); + if (req.method === 'GET' && (await serveStatic(url.pathname, res))) return; + const match = router.match(req.method ?? 'GET', url.pathname); + if (match.kind === 'not-found') { + throw new HttpError(404, 'NOT_FOUND', `No existe la ruta ${url.pathname}.`); + } + if (match.kind === 'method-not-allowed') { + throw new HttpError(405, 'METHOD_NOT_ALLOWED', `${req.method} no está permitido en ${url.pathname}.`); + } + await match.handler({ req, res, url, params: match.params }); + }; +} + +export function createServer(options: ServerOptions): http.Server { + const deps: AppDependencies = { + service: new BookingService(options.db), + clock: options.clock ?? systemClock, + notifier: options.notifier ?? consoleNotifier, + }; + const log = options.log ?? console.log; + const router = buildRouter(deps); + + return http.createServer(withLog(log, withErrors(dispatcher(router)))); }