import Fastify, { FastifyInstance } from 'fastify'; import { AppError } from './utils/errors.js'; import databasePlugin from './plugins/database.js'; import redisPlugin from './plugins/redis.js'; import securityPlugin from './plugins/security.js'; import rateLimitPlugin from './plugins/rateLimit.js'; import { env } from './config/env.js'; import { randomUUID } from 'node:crypto'; export async function buildApp(): Promise { const isDev = env.NODE_ENV === 'development'; const app = Fastify({ logger: { level: isDev ? 'debug' : 'info', transport: isDev ? { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, } : undefined, }, requestIdHeader: 'x-request-id', requestIdLogLabel: 'requestId', genReqId: () => randomUUID(), }); app.setErrorHandler((err: unknown, request, reply) => { const error = err as Error & { statusCode?: number; validation?: unknown }; request.log.error({ err }, error.message); if (err instanceof AppError) { const statusCode = err.statusCode; const payload = err.toJSON(); if (err.code === 'RATE_LIMIT_EXCEEDED' && 'retryAfter' in err) { reply.header('Retry-After', String((err as AppError & { retryAfter?: number }).retryAfter ?? 60)); } return reply.status(statusCode).send(payload); } if (error.validation) { return reply.status(422).send({ error: { code: 'VALIDATION_ERROR', message: 'Validation failed', details: error.validation, }, }); } const statusCode = error.statusCode ?? 500; return reply.status(statusCode).send({ error: { code: 'INTERNAL_ERROR', message: env.NODE_ENV === 'production' ? 'Internal server error' : error.message, }, }); }); app.addHook('onRequest', async (request, reply) => { reply.header('x-request-id', request.id); }); await app.register(redisPlugin); await app.register(databasePlugin); await app.register(securityPlugin); await app.register(rateLimitPlugin); app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() })); return app; }