/** * Centralized error handling utility for API routes * * Standard error format: * { * error: 'ErrorType', * message: 'Human-readable description', * field: 'optional-field-name', * code: 'machine-readable-code' * } */ import type { Req, Res, Next } from '../types/http'; class ApiError extends Error { code: string; status: number; details: any; field?: string; constructor(code: string, message: string, status = 500, details: any = {}) { super(message); this.name = 'ApiError'; this.code = code; this.status = status; this.details = details; // Extract field name from details if provided if (details.field) { this.field = details.field; } } } /** * Create a standardized validation error */ function ValidationError(message: string, field?: string, code = 'VALIDATION_ERROR'): ApiError { return new ApiError(code, message, 400, { field }); } /** * Create a standardized authentication error */ function AuthError(message = 'Authentication required', code = 'AUTH_ERROR'): ApiError { return new ApiError(code, message, 401); } /** * Create a standardized authorization error */ function ForbiddenError(message = 'Access denied', code = 'FORBIDDEN'): ApiError { return new ApiError(code, message, 403); } /** * Create a standardized not found error */ function NotFoundError( message = 'Resource not found', field?: string, code = 'NOT_FOUND', ): ApiError { return new ApiError(code, message, 404, { field }); } /** * Create a standardized conflict error */ function ConflictError(message = 'Resource conflict', field?: string, code = 'CONFLICT'): ApiError { return new ApiError(code, message, 409, { field }); } /** * Create a standardized rate limit error */ function RateLimitError(message = 'Too many requests', code = 'RATE_LIMITED'): ApiError { return new ApiError(code, message, 429); } /** * Format an error for JSON response * This ensures consistent error format across all routes */ function formatError(err: any): { error: string; message: string; code: string; field?: string } { if (err instanceof ApiError) { // `error` and `code` are both the machine code (the client reads `code`); // `error` is kept for backward compatibility. const output: { error: string; message: string; code: string; field?: string } = { error: err.code, message: err.message, code: err.code, }; if (err.field) output.field = err.field; return output; } // Non-ApiError: hide internals on any 5xx (missing status is treated as 500). const status = err.status || 500; const code = err.code || 'INTERNAL_ERROR'; const safeMessage = status >= 500 ? 'Internal server error' : err.message || 'An error occurred'; return { error: code, message: safeMessage, code, }; } /** * Express middleware to handle errors centrally */ function errorHandler(err: any, req: Req, res: Res, next: Next): void { const formatted = formatError(err); const status = err.status || (err instanceof ApiError ? 500 : 500); // Only log non-500 errors to avoid exposing sensitive info if (status !== 500) { console.error(`[error] ${formatted.error}: ${formatted.message}`); } if (!res.headersSent) { res.status(status).json(formatted); } } module.exports = { ApiError, ValidationError, AuthError, ForbiddenError, NotFoundError, ConflictError, RateLimitError, formatError, errorHandler, };