Lesson 23 of 30
A form isn't done when the fields look right. Between 'the user clicked submit' and 'the server accepted it' sits a real, deliberate stretch of code — this is the data-layer half of that stretch, not the UI half.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A form tracks a birth date as three separate fields (day, month, year) because that's convenient for the UI, but the API expects one ISO date string. What has to happen for the request to succeed?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Forms get a lot of attention as a UI and accessibility problem — labels, focus order, error messaging. This lesson stays on the narrower, data-layer slice: what happens to the data itself between a user typing into fields and the server receiving a request it can actually use. Four concerns live here:
type FormState = {
email: string;
birthDay: string;
birthMonth: string;
birthYear: string;
};
type ApiPayload = {
email: string;
birthDate: string; // ISO format, e.g. "1994-03-21"
};
// The transform step: form-convenient shape -> API-expected shape.
function toPayload(form: FormState): ApiPayload {
const month = form.birthMonth.padStart(2, "0");
const day = form.birthDay.padStart(2, "0");
return {
email: form.email.trim(),
birthDate: `${form.birthYear}-${month}-${day}`,
};
}
function useSignupForm() {
const [form, setForm] = useState<FormState>({ email: "", birthDay: "", birthMonth: "", birthYear: "" });
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [submitting, setSubmitting] = useState(false);
async function handleSubmit() {
if (submitting) return; // guards against a double-click firing this twice
setSubmitting(true);
setFieldErrors({});
try {
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(toPayload(form)),
});
if (!res.ok) {
// The server's validation is the one that actually counts. Its response
// shape is keyed by field name, so route each message back to that field.
const body = await res.json(); // e.g. { errors: { email: "already in use" } }
setFieldErrors(body.errors ?? {});
return;
}
// handle success
} finally {
setSubmitting(false);
}
}
return { form, setForm, fieldErrors, submitting, handleSubmit };
}
fieldErrors.email can now be read directly by the email input's own error text, rather than a single banner saying something failed somewhere. The submitting flag both disables the submit control and prevents handleSubmit from re-entering while a request is already in flight.
"Walk me through what happens from clicking submit to the server responding" is a common way to probe whether a candidate has actually built a real form or only styled one — the transform step and the double-submission guard are the two details that separate the two.