refactor(api): extract buildJsonErrorResponse from apiError and returnAllZodIssues

The W2 `returnAllZodIssues` helper duplicated the meta/headers handling
from `apiError`: same `{headers, ...rest}` destructure, same `metaKeys`
length check, same `...(headers ? {headers} : {})` spread on the
constructor. Six lines that drift the moment one helper gains a feature
(e.g. an `errorId` autoinject for Sentry) the other doesn't.

Extract the shared body into `buildJsonErrorResponse(body, status,
meta)`. Both `apiError` and `returnAllZodIssues` collapse to one-line
constructions of the resource-specific body shape. Wire shape is
byte-identical — the 7-case `api-response-zod` suite and every
downstream `apiError` consumer still see the same envelopes.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 19:27:20 +02:00
1 parent a5176f4be9
commit d2adf265e4
1 file changed
+36 -29
+36 -29
View File
@@ -44,23 +44,29 @@ export function sanitiseZodIssues(
}));
}
export function returnAllZodIssues(
error: ZodError,
status: number = 422,
meta?: {
errorCode?: string;
headers?: Record<string, string>;
} & Record<string, unknown>,
type ErrorMeta = {
errorCode?: string;
headers?: Record<string, string>;
} & Record<string, unknown>;
/**
* Shared builder for every `{ data: null, error, ... }` JSON envelope.
* Strips `headers` from the meta passthrough (it lands on the
* NextResponse constructor, not in the JSON body) and omits the `meta`
* key entirely when no non-header fields remain — so the unchanged
* `{ data: null, error: <string> }` envelope still serialises byte-
* identically when the caller passes no extras.
*/
function buildJsonErrorResponse(
body: Record<string, unknown>,
status: number,
meta: ErrorMeta | undefined,
): NextResponse {
const { headers, ...rest } = meta ?? {};
const metaKeys = Object.keys(rest);
return NextResponse.json(
{
data: null,
error: "Validation failed",
details: {
issues: sanitiseZodIssues(error.issues),
},
...body,
...(metaKeys.length > 0 ? { meta: rest } : {}),
},
{
@@ -70,6 +76,22 @@ export function returnAllZodIssues(
);
}
export function returnAllZodIssues(
error: ZodError,
status: number = 422,
meta?: ErrorMeta,
): NextResponse {
return buildJsonErrorResponse(
{
data: null,
error: "Validation failed",
details: { issues: sanitiseZodIssues(error.issues) },
},
status,
meta,
);
}
/**
* `meta` is additive — clients that ignore it see the unchanged
* `{ data: null, error: <string> }` envelope. New callers can pass
@@ -86,24 +108,9 @@ export function returnAllZodIssues(
export function apiError(
message: string,
status = 400,
meta?: {
errorCode?: string;
headers?: Record<string, string>;
} & Record<string, unknown>,
meta?: ErrorMeta,
) {
const { headers, ...rest } = meta ?? {};
const metaKeys = Object.keys(rest);
return NextResponse.json(
{
data: null,
error: message,
...(metaKeys.length > 0 ? { meta: rest } : {}),
},
{
status,
...(headers ? { headers } : {}),
},
);
return buildJsonErrorResponse({ data: null, error: message }, status, meta);
}
/**