diff --git a/src/middlewares/error.middleware.ts b/src/middlewares/error.middleware.ts index 0f9b091..2846e03 100644 --- a/src/middlewares/error.middleware.ts +++ b/src/middlewares/error.middleware.ts @@ -1,4 +1,3 @@ -// src/middlewares/error.middleware.ts import { NextFunction, Request, Response } from 'express'; import { envConfig } from '../config'; import { ErrorRequestHandler } from 'express'; @@ -9,6 +8,7 @@ import { logger } from '../utils/logger.utils'; import { mapUnknownRouteError } from '../utils/route-error.utils'; import { buildErrorContext } from '../utils/error-context.utils'; import { sanitizeLogFieldValue } from '../utils/log-field-sanitizer.utils'; +import { buildErrorResponse, zodIssuesToDetails } from '../utils/api-response.utils'; export class ApiError extends Error { statusCode: number; @@ -73,12 +73,14 @@ export const errorHandler: ErrorRequestHandler = ( // Handle Zod validation errors if (err instanceof z.ZodError || err.name === 'ZodError') { - res.status(400).json({ - success: false, - code: ErrorCode.VALIDATION_ERROR, - message: 'Validation failed', - errors: err.errors || err.issues, - }); + const issues: z.ZodIssue[] = err.errors ?? err.issues ?? []; + res.status(400).json( + buildErrorResponse( + ErrorCode.VALIDATION_ERROR, + 'Validation failed', + zodIssuesToDetails(issues) + ) + ); return; } diff --git a/src/modules/creator/creator-profile.handlers.ts b/src/modules/creator/creator-profile.handlers.ts index 46f164a..8c25834 100644 --- a/src/modules/creator/creator-profile.handlers.ts +++ b/src/modules/creator/creator-profile.handlers.ts @@ -3,6 +3,7 @@ import { sendError, sendSuccess, sendValidationError, + zodIssuesToDetails, ErrorCode, } from '../../utils/api-response.utils'; import { logger } from '../../utils/logger.utils'; @@ -30,10 +31,7 @@ export async function getCreatorProfileHandler(req: Request, res: Response) { return sendValidationError( res, 'Invalid creator profile path parameters', - paramsResult.error.issues.map(issue => ({ - field: issue.path.join('.'), - message: issue.message, - })) + zodIssuesToDetails(paramsResult.error.issues) ); } @@ -72,10 +70,7 @@ export async function upsertCreatorProfileHandler(req: Request, res: Response) { return sendValidationError( res, 'Invalid creator profile path parameters', - paramsResult.error.issues.map(issue => ({ - field: issue.path.join('.'), - message: issue.message, - })) + zodIssuesToDetails(paramsResult.error.issues) ); } @@ -84,10 +79,7 @@ export async function upsertCreatorProfileHandler(req: Request, res: Response) { return sendValidationError( res, 'Invalid creator profile payload', - bodyResult.error.issues.map(issue => ({ - field: issue.path.join('.'), - message: issue.message, - })) + zodIssuesToDetails(bodyResult.error.issues) ); } diff --git a/src/utils/api-response.utils.ts b/src/utils/api-response.utils.ts index 68c2b67..c0bdddb 100644 --- a/src/utils/api-response.utils.ts +++ b/src/utils/api-response.utils.ts @@ -2,6 +2,7 @@ // Shared API response formatters for consistent client-facing responses. import { Response } from 'express'; +import { ZodIssue } from 'zod'; import { ErrorCode, ErrorCodeType } from '../constants/error.constants'; import { requestContextStorage } from './als.utils'; @@ -153,6 +154,24 @@ export function sendPaginatedSuccess( // ── Convenience helpers ────────────────────────────────────── +/** + * Maps Zod issues to the standard `details` array used in error responses. + * + * @example + * const result = schema.safeParse(input); + * if (!result.success) { + * return sendValidationError(res, 'Invalid input', zodIssuesToDetails(result.error.issues)); + * } + */ +export function zodIssuesToDetails( + issues: ZodIssue[] +): Array<{ field: string; message: string }> { + return issues.map(issue => ({ + field: issue.path.join('.'), + message: issue.message, + })); +} + export function sendValidationError( res: Response, message: string, diff --git a/src/utils/test/api-response.utils.test.ts b/src/utils/test/api-response.utils.test.ts index d68d57f..5f4db55 100644 --- a/src/utils/test/api-response.utils.test.ts +++ b/src/utils/test/api-response.utils.test.ts @@ -3,6 +3,7 @@ import { sendForbidden, sendUnauthorized, buildErrorResponse, + zodIssuesToDetails, ErrorCode, } from '../api-response.utils'; import { requestContextStorage } from '../als.utils'; @@ -130,3 +131,41 @@ describe('buildErrorResponse', () => { expect(body!.requestId).toBe(capturedRequestId); }); }); + +describe('zodIssuesToDetails', () => { + it('maps a single issue to a details entry', () => { + const result = zodIssuesToDetails([ + { path: ['email'], message: 'Invalid email', code: 'invalid_string' } as any, + ]); + expect(result).toEqual([{ field: 'email', message: 'Invalid email' }]); + }); + + it('joins nested paths with a dot', () => { + const result = zodIssuesToDetails([ + { path: ['address', 'city'], message: 'Required', code: 'invalid_type' } as any, + ]); + expect(result).toEqual([{ field: 'address.city', message: 'Required' }]); + }); + + it('produces an empty string field for root-level issues', () => { + const result = zodIssuesToDetails([ + { path: [], message: 'Input must be an object', code: 'invalid_type' } as any, + ]); + expect(result).toEqual([{ field: '', message: 'Input must be an object' }]); + }); + + it('returns an empty array for an empty issues list', () => { + expect(zodIssuesToDetails([])).toEqual([]); + }); + + it('maps multiple issues preserving order', () => { + const result = zodIssuesToDetails([ + { path: ['name'], message: 'Required', code: 'invalid_type' } as any, + { path: ['age'], message: 'Must be a number', code: 'invalid_type' } as any, + ]); + expect(result).toEqual([ + { field: 'name', message: 'Required' }, + { field: 'age', message: 'Must be a number' }, + ]); + }); +});