Interview question
Map API field errors into a form
Maps a stable ProblemDetails validation contract to fields and a fallback form-level error.
TL;DR
Maps a stable ProblemDetails validation contract to fields and a fallback form-level error.
ProblemDetails mapping, stable field keys, unknown errors, focus, and preserving form values.
Practice the problem like a real interview: restate, reason, implement, and test.
Submit a create-user form to an API returning { code, errors, traceId }. Known keys attach to fields; unknown keys and non-validation failures appear in a form alert.
A duplicate email error appears beside email; an unknown tenant error remains visible in the form alert.
I parse only the public error contract, split known field keys from the remainder, then focus the summary. A transport failure gets recovery wording rather than pretending it is validation.
async function submit(values: CreateUserInput) {
setPending(true);
setFieldErrors({});
setFormError(null);
try {
const response = await fetch("/api/users", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify(values)
});
if (response.ok) return router.push("/users");
const problem = await response.json().catch(() => null) as ApiProblem | null;
if (response.status === 400 && problem?.errors) {
const known = pickKnownFields(problem.errors, ["displayName", "email"]);
setFieldErrors(known.fields);
setFormError(known.other.length ? known.other.join(" ") : null);
} else {
setFormError(`Save failed. Reference: ${problem?.traceId ?? "unavailable"}`);
}
requestAnimationFrame(() => summaryRef.current?.focus());
} catch {
setFormError("The service could not be reached. Try again.");
requestAnimationFrame(() => summaryRef.current?.focus());
} finally { setPending(false); }
}
The mapper depends on stable machine keys, not message prose. Unknown fields remain visible so backend contract changes do not silently disappear.
Next in Practical UI Labs: Single Submission