Skip to content

Commit 03f3656

Browse files
authored
Merge pull request #361 from K1NGD4VID/consistent-validation
feat: implement global error handling middleware, creator profile han…
2 parents 01309d9 + a0fb898 commit 03f3656

4 files changed

Lines changed: 71 additions & 19 deletions

File tree

src/middlewares/error.middleware.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// src/middlewares/error.middleware.ts
21
import { NextFunction, Request, Response } from 'express';
32
import { envConfig } from '../config';
43
import { ErrorRequestHandler } from 'express';
@@ -9,6 +8,7 @@ import { logger } from '../utils/logger.utils';
98
import { mapUnknownRouteError } from '../utils/route-error.utils';
109
import { buildErrorContext } from '../utils/error-context.utils';
1110
import { sanitizeLogFieldValue } from '../utils/log-field-sanitizer.utils';
11+
import { buildErrorResponse, zodIssuesToDetails } from '../utils/api-response.utils';
1212

1313
export class ApiError extends Error {
1414
statusCode: number;
@@ -73,12 +73,14 @@ export const errorHandler: ErrorRequestHandler = (
7373

7474
// Handle Zod validation errors
7575
if (err instanceof z.ZodError || err.name === 'ZodError') {
76-
res.status(400).json({
77-
success: false,
78-
code: ErrorCode.VALIDATION_ERROR,
79-
message: 'Validation failed',
80-
errors: err.errors || err.issues,
81-
});
76+
const issues: z.ZodIssue[] = err.errors ?? err.issues ?? [];
77+
res.status(400).json(
78+
buildErrorResponse(
79+
ErrorCode.VALIDATION_ERROR,
80+
'Validation failed',
81+
zodIssuesToDetails(issues)
82+
)
83+
);
8284
return;
8385
}
8486

src/modules/creator/creator-profile.handlers.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
sendError,
44
sendSuccess,
55
sendValidationError,
6+
zodIssuesToDetails,
67
ErrorCode,
78
} from '../../utils/api-response.utils';
89
import { logger } from '../../utils/logger.utils';
@@ -30,10 +31,7 @@ export async function getCreatorProfileHandler(req: Request, res: Response) {
3031
return sendValidationError(
3132
res,
3233
'Invalid creator profile path parameters',
33-
paramsResult.error.issues.map(issue => ({
34-
field: issue.path.join('.'),
35-
message: issue.message,
36-
}))
34+
zodIssuesToDetails(paramsResult.error.issues)
3735
);
3836
}
3937

@@ -72,10 +70,7 @@ export async function upsertCreatorProfileHandler(req: Request, res: Response) {
7270
return sendValidationError(
7371
res,
7472
'Invalid creator profile path parameters',
75-
paramsResult.error.issues.map(issue => ({
76-
field: issue.path.join('.'),
77-
message: issue.message,
78-
}))
73+
zodIssuesToDetails(paramsResult.error.issues)
7974
);
8075
}
8176

@@ -84,10 +79,7 @@ export async function upsertCreatorProfileHandler(req: Request, res: Response) {
8479
return sendValidationError(
8580
res,
8681
'Invalid creator profile payload',
87-
bodyResult.error.issues.map(issue => ({
88-
field: issue.path.join('.'),
89-
message: issue.message,
90-
}))
82+
zodIssuesToDetails(bodyResult.error.issues)
9183
);
9284
}
9385

src/utils/api-response.utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Shared API response formatters for consistent client-facing responses.
33

44
import { Response } from 'express';
5+
import { ZodIssue } from 'zod';
56
import { ErrorCode, ErrorCodeType } from '../constants/error.constants';
67
import { requestContextStorage } from './als.utils';
78

@@ -153,6 +154,24 @@ export function sendPaginatedSuccess<T>(
153154

154155
// ── Convenience helpers ──────────────────────────────────────
155156

157+
/**
158+
* Maps Zod issues to the standard `details` array used in error responses.
159+
*
160+
* @example
161+
* const result = schema.safeParse(input);
162+
* if (!result.success) {
163+
* return sendValidationError(res, 'Invalid input', zodIssuesToDetails(result.error.issues));
164+
* }
165+
*/
166+
export function zodIssuesToDetails(
167+
issues: ZodIssue[]
168+
): Array<{ field: string; message: string }> {
169+
return issues.map(issue => ({
170+
field: issue.path.join('.'),
171+
message: issue.message,
172+
}));
173+
}
174+
156175
export function sendValidationError(
157176
res: Response,
158177
message: string,

src/utils/test/api-response.utils.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
sendForbidden,
44
sendUnauthorized,
55
buildErrorResponse,
6+
zodIssuesToDetails,
67
ErrorCode,
78
} from '../api-response.utils';
89
import { requestContextStorage } from '../als.utils';
@@ -130,3 +131,41 @@ describe('buildErrorResponse', () => {
130131
expect(body!.requestId).toBe(capturedRequestId);
131132
});
132133
});
134+
135+
describe('zodIssuesToDetails', () => {
136+
it('maps a single issue to a details entry', () => {
137+
const result = zodIssuesToDetails([
138+
{ path: ['email'], message: 'Invalid email', code: 'invalid_string' } as any,
139+
]);
140+
expect(result).toEqual([{ field: 'email', message: 'Invalid email' }]);
141+
});
142+
143+
it('joins nested paths with a dot', () => {
144+
const result = zodIssuesToDetails([
145+
{ path: ['address', 'city'], message: 'Required', code: 'invalid_type' } as any,
146+
]);
147+
expect(result).toEqual([{ field: 'address.city', message: 'Required' }]);
148+
});
149+
150+
it('produces an empty string field for root-level issues', () => {
151+
const result = zodIssuesToDetails([
152+
{ path: [], message: 'Input must be an object', code: 'invalid_type' } as any,
153+
]);
154+
expect(result).toEqual([{ field: '', message: 'Input must be an object' }]);
155+
});
156+
157+
it('returns an empty array for an empty issues list', () => {
158+
expect(zodIssuesToDetails([])).toEqual([]);
159+
});
160+
161+
it('maps multiple issues preserving order', () => {
162+
const result = zodIssuesToDetails([
163+
{ path: ['name'], message: 'Required', code: 'invalid_type' } as any,
164+
{ path: ['age'], message: 'Must be a number', code: 'invalid_type' } as any,
165+
]);
166+
expect(result).toEqual([
167+
{ field: 'name', message: 'Required' },
168+
{ field: 'age', message: 'Must be a number' },
169+
]);
170+
});
171+
});

0 commit comments

Comments
 (0)