77 lines
2.3 KiB
TypeScript
77 lines
2.3 KiB
TypeScript
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<FastifyInstance> {
|
|
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;
|
|
}
|