# Phase 7: API Server Refactor + Composition Root ## Middleware stack (ORDEN IMPORTA) ```typescript // server.ts export function createServer(deps: ServerDependencies): Express { const app = express(); // 1. Request ID (PRIMERO — todo log necesita esto) app.use(requestIdMiddleware); // 2. Security headers app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], connectSrc: ["'self'", "ws:", "wss:"], scriptSrc: ["'self'", "'unsafe-inline'"], // para Scalar docs }, }, })); // 3. CORS app.use(cors({ origin: deps.config.cors.origin, credentials: true, })); // 4. Rate limiting global app.use(rateLimit({ windowMs: deps.config.api.rateLimitWindowMs, max: deps.config.api.rateLimitMax, standardHeaders: true, legacyHeaders: false, })); // 5. Body parsing app.use(express.json({ limit: '10mb' })); // 6. Health endpoints (SIN auth) app.get('/health/live', (_, res) => res.json({ status: 'ok' })); app.get('/health/ready', async (_, res) => { /* check DB */ }); // 7. Auth routes (SIN auth middleware general) app.use('/api/auth', deps.authController.router); // 8. Auth middleware (TODOS los /api/ a partir de aquí) app.use('/api', deps.authMiddleware); // 9. Module routes app.use('/api', deps.crawlingController.router); app.use('/api', deps.findingsController.router); app.use('/api', deps.fuzzingController.router); // ... más módulos // 10. 404 handler app.use(notFoundMiddleware); // 11. Error handler (SIEMPRE último) app.use(globalErrorHandler); return app; } ``` ## Error hierarchy ```typescript export class AppError extends Error { constructor( message: string, public readonly statusCode: number, public readonly code: string, public readonly isOperational = true, ) { super(message); } } export class ValidationError extends AppError { constructor(message: string, public readonly details?: unknown) { super(message, 400, 'VALIDATION_ERROR'); } } export class AuthenticationError extends AppError { constructor(message = 'Unauthorized') { super(message, 401, 'AUTHENTICATION_ERROR'); } } export class ForbiddenError extends AppError { constructor(message = 'Forbidden') { super(message, 403, 'FORBIDDEN'); } } export class NotFoundError extends AppError { constructor(resource: string) { super(`${resource} not found`, 404, 'NOT_FOUND'); } } export class ConflictError extends AppError { constructor(message: string) { super(message, 409, 'CONFLICT'); } } export class RateLimitError extends AppError { constructor() { super('Too many requests', 429, 'RATE_LIMIT'); } } ``` ## Global error handler ```typescript export function globalErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) { const logger = req.log || console; // pino child logger if (err instanceof AppError && err.isOperational) { logger.warn({ err, statusCode: err.statusCode }, err.message); return res.status(err.statusCode).json({ error: err.message, code: err.code, ...(err instanceof ValidationError && err.details ? { details: err.details } : {}), }); } // Programmer error — log full stack, return generic message logger.error({ err }, 'Unhandled error'); return res.status(500).json({ error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message, code: 'INTERNAL_ERROR', }); } ``` ## Composition root (main.ts) ```typescript async function bootstrap() { // 1. Config const config = loadConfig(); // 2. Logger const logger = createLogger(config); logger.info({ port: config.port }, 'Starting ABE...'); // 3. Database + migrations const db = createDatabase(config.db); await runMigrations(db, logger); // 4. Event bus const eventBus = new InProcessEventBus(logger); // 5. Storage const storage = new LocalStorageProvider(config.storage.path); // 6. Repositories const sessionRepo = new KyselyCrawlSessionRepository(db); const findingRepo = new KyselyFindingRepository(db); // ... etc // 7. Use cases const startCrawl = new StartCrawlCommand(sessionRepo, eventBus); const listFindings = new ListFindingsQuery(findingRepo); // ... etc // 8. Event handlers — subscribe to event bus const onAnomalyDetected = new OnAnomalyDetected(findingRepo, eventBus); eventBus.subscribe('crawling.anomaly_detected', onAnomalyDetected); // ... etc // 9. Controllers const crawlingController = new CrawlingController(startCrawl, ...); const findingsController = new FindingsController(listFindings, ...); // ... etc // 10. HTTP server const app = createServer({ config, authMiddleware, crawlingController, findingsController, ... }); const httpServer = createServer(app); // 11. Socket.io const io = new Server(httpServer, { cors: { origin: config.cors.origin } }); const gateway = new SocketGateway(io, eventBus); // 12. Job queue const jobQueue = new SQLiteJobQueue(db, logger); jobQueue.start(); // 13. Listen httpServer.listen(config.port, config.host, () => { logger.info({ port: config.port }, 'ABE server ready'); }); // 14. Graceful shutdown async function shutdown(signal: string) { logger.info({ signal }, 'Shutting down...'); httpServer.close(); io.close(); jobQueue.pause(); await jobQueue.waitForActive(30000); await db.destroy(); logger.info('Shutdown complete'); process.exit(0); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); } bootstrap().catch((err) => { console.error('Fatal: failed to start ABE', err); process.exit(1); }); ``` ## IMPORTANTE - El código existente en src/server/ debe DEJAR DE USARSE gradualmente - Mantener los endpoints viejos funcionando durante la migración - Cada controller es una clase con un `.router` getter que retorna Express.Router - NUNCA meter lógica de negocio en controllers — solo parse request → call use case → format response