fase(7): api server refactor with composition root

This commit is contained in:
debian
2026-03-05 09:36:28 -05:00
parent e746dc0497
commit f01acfe985
20 changed files with 861 additions and 2 deletions

89
src/api/server.ts Normal file
View File

@@ -0,0 +1,89 @@
/**
* ABE API Server — Express app factory.
* Middleware order matters: requestId → helmet → cors → rateLimit → body → routes → notFound → errorHandler
*/
import express, { Express, Request, Response } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { Kysely } from 'kysely';
import { AppConfig } from '../shared/infrastructure/Config';
import { Logger } from '../shared/infrastructure/Logger';
import { Database } from '../shared/infrastructure/DatabaseConnection';
import { createRequestIdMiddleware } from './middleware/requestId';
import { notFoundMiddleware } from './middleware/notFound';
import { globalErrorHandler } from './middleware/errorHandler';
import { createRouter } from './router';
import { CrawlingControllerDeps } from '../modules/crawling/infrastructure/http/CrawlingController';
import { FindingsControllerDeps } from '../modules/findings/infrastructure/http/FindingsController';
import { FuzzingControllerDeps } from '../modules/fuzzing/infrastructure/http/FuzzingController';
export interface ServerDependencies {
config: AppConfig;
logger: Logger;
db: Kysely<Database>;
crawlingDeps: CrawlingControllerDeps;
findingsDeps: FindingsControllerDeps;
fuzzingDeps: FuzzingControllerDeps;
}
export function createServer(deps: ServerDependencies): Express {
const app = express();
// 1. Request ID — must be first so all logs have requestId
app.use(createRequestIdMiddleware(deps.logger));
// 2. Security headers
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
connectSrc: ["'self'", 'ws:', 'wss:'],
scriptSrc: ["'self'", "'unsafe-inline'"],
},
},
}),
);
// 3. CORS
app.use(cors({ origin: deps.config.cors.origin, credentials: true }));
// 4. Rate limiting
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 — no auth required
app.get('/health/live', (_req: Request, res: Response) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
app.get('/health/ready', async (_req: Request, res: Response) => {
try {
await deps.db.selectFrom('sessions').select('id').limit(1).execute();
res.json({ status: 'ready', db: 'connected' });
} catch (err) {
res.status(503).json({ status: 'not_ready', db: 'disconnected', error: String(err) });
}
});
// 7. Module routes
app.use('/api', createRouter(deps));
// 8. 404 handler
app.use(notFoundMiddleware);
// 9. Global error handler — always last
app.use(globalErrorHandler);
return app;
}