feat: add Fastify app with error handler
Made-with: Cursor
This commit is contained in:
76
src/app.ts
Normal file
76
src/app.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user