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 };
|
||||
@@ -0,0 +1,21 @@
|
||||
const { rateLimit } = require("express-rate-limit");
|
||||
|
||||
const response = { success: false, error: { code: "RATE_LIMITED", message: "Too many requests" } };
|
||||
|
||||
const generalApiLimiter = rateLimit({
|
||||
windowMs: Number(process.env.API_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
||||
limit: Number(process.env.API_RATE_LIMIT_MAX) || 300,
|
||||
standardHeaders: "draft-8",
|
||||
legacyHeaders: false,
|
||||
message: response,
|
||||
});
|
||||
|
||||
const sensitiveLimiter = rateLimit({
|
||||
windowMs: Number(process.env.SENSITIVE_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
||||
limit: Number(process.env.SENSITIVE_RATE_LIMIT_MAX) || 20,
|
||||
standardHeaders: "draft-8",
|
||||
legacyHeaders: false,
|
||||
message: response,
|
||||
});
|
||||
|
||||
module.exports = { generalApiLimiter, sensitiveLimiter };
|
||||
@@ -0,0 +1,10 @@
|
||||
const { randomUUID } = require("crypto");
|
||||
|
||||
const SAFE_REQUEST_ID = /^[A-Za-z0-9_-]{8,128}$/;
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
const supplied = req.get("x-request-id");
|
||||
req.id = supplied && SAFE_REQUEST_ID.test(supplied) ? supplied : randomUUID();
|
||||
res.setHeader("X-Request-ID", req.id);
|
||||
next();
|
||||
};
|
||||
Reference in New Issue
Block a user