Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions src/middlewares/error.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// src/middlewares/error.middleware.ts
import { NextFunction, Request, Response } from 'express';
import { envConfig } from '../config';
import { ErrorRequestHandler } from 'express';
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
16 changes: 4 additions & 12 deletions src/modules/creator/creator-profile.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
sendError,
sendSuccess,
sendValidationError,
zodIssuesToDetails,
ErrorCode,
} from '../../utils/api-response.utils';
import { logger } from '../../utils/logger.utils';
Expand Down Expand Up @@ -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)
);
}

Expand Down Expand Up @@ -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)
);
}

Expand All @@ -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)
);
}

Expand Down
19 changes: 19 additions & 0 deletions src/utils/api-response.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -153,6 +154,24 @@ export function sendPaginatedSuccess<T>(

// ── 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,
Expand Down
39 changes: 39 additions & 0 deletions src/utils/test/api-response.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
sendForbidden,
sendUnauthorized,
buildErrorResponse,
zodIssuesToDetails,
ErrorCode,
} from '../api-response.utils';
import { requestContextStorage } from '../als.utils';
Expand Down Expand Up @@ -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' },
]);
});
});
Loading