feat: stabilize API startup and lifecycle management
- Refactor server initialization to separate concerns and improve error handling. - Implement centralized environment validation using Zod. - Introduce database, Redis, and queue lifecycle management. - Add health check endpoints for liveness and readiness. - Enhance error handling middleware for better response structure. - Implement rate limiting for API endpoints. - Add request ID middleware for traceability. - Create Sequelize CLI configuration and baseline migration for schema management. - Establish CI workflow with Gitea for testing and syntax checks. - Document foundational changes and migration strategy in PHASE_0_FOUNDATION_STABILIZATION.md. - Add Docker Compose configuration for local development and testing. - Implement unit and integration tests for critical functionality.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
const { ValidationError, UniqueConstraintError } = require("sequelize");
|
||||
const { ZodError } = require("zod");
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(status, code, message, details) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
const notFound = (req, _res, next) => next(new AppError(404, "NOT_FOUND", "Route not found"));
|
||||
|
||||
const errorHandler = (error, req, res, _next) => {
|
||||
let status = error.status || error.statusCode || 500;
|
||||
let code = error.code || "INTERNAL_ERROR";
|
||||
let message = error.message || "An unexpected error occurred";
|
||||
let details = error.details;
|
||||
|
||||
if (error instanceof ZodError) {
|
||||
status = 400; code = "VALIDATION_ERROR"; message = "Invalid request data";
|
||||
details = error.issues.map(({ path, message: detailMessage }) => ({ field: path.join("."), message: detailMessage }));
|
||||
} else if (error instanceof UniqueConstraintError) {
|
||||
status = 409; code = "CONFLICT"; message = "A record with these values already exists";
|
||||
details = error.errors?.map(({ path, message: detailMessage }) => ({ field: path, message: detailMessage }));
|
||||
} else if (error instanceof ValidationError) {
|
||||
status = 400; code = "VALIDATION_ERROR"; message = "Invalid request data";
|
||||
details = error.errors?.map(({ path, message: detailMessage }) => ({ field: path, message: detailMessage }));
|
||||
} else if (error.type === "entity.too.large") {
|
||||
status = 413; code = "PAYLOAD_TOO_LARGE"; message = "Request body is too large";
|
||||
} else if (error.name === "UnauthorizedError" || error.name === "JsonWebTokenError") {
|
||||
status = 401; code = "UNAUTHORIZED"; message = "Authentication failed";
|
||||
}
|
||||
|
||||
if (status >= 500) {
|
||||
console.error(`[${req.id || "no-request-id"}] Request failed`, { name: error.name, message: error.message });
|
||||
if (process.env.NODE_ENV === "production") message = "An unexpected error occurred";
|
||||
}
|
||||
|
||||
const payload = { success: false, error: { code, message }, requestId: req.id };
|
||||
if (details && status < 500) payload.error.details = details;
|
||||
res.status(status).json(payload);
|
||||
};
|
||||
|
||||
module.exports = { AppError, notFound, errorHandler };
|
||||
Reference in New Issue
Block a user