Module 7 · Web Development with JavaScript
Form validation
Validate a form in the browser so people get fast, clear, accessible error messages, and understand why the server must validate everything again anyway.
About 25 minutes
The problem
The payment form must stop obvious mistakes before they're sent: an empty reference, an amount with letters, more than what's owed. But the old page only showed a red border with no text, so screen reader users never knew what was wrong, and some customers gave up.
There's a second lesson hiding here: browser validation is for people, not for security. Anyone can send a request to the API without using the page at all.
The concept
Validate in two places
| Where | Purpose |
|---|---|
| Browser | fast, friendly feedback while people type or submit |
| Server (the API) | the real rules, for every request, from any client |
The API from the Databases and APIs course already refuses bad payments with 400 and 409. The page's checks are a courtesy on top.
Good error messages
- Say what's wrong and how to fix it ("Enter the reference from your bank transfer"), not just "Invalid".
- Show them next to the field, and link them with
aria-describedby, so they're announced. - Mark the field with
aria-invalid="true", and move focus to the first field with an error on submit. - Don't rely on colour alone.
Built-in or custom
HTML attributes (required, type="email", pattern) give basic checks for free. For messages you control, add novalidate to the form and validate in JavaScript.
Example
The validation as a plain function that returns every problem, keyed by field:
JavaScript
function validatePayment({ amount, reference }, dueKobo) {
const errors = {};
const cleaned = amount.replace(/[₦,\s]/g, "");
if (cleaned === "") {
errors.amount = "Enter how much you're paying";
} else if (!/^\d+(\.\d{1,2})?$/.test(cleaned)) {
errors.amount = "Enter an amount in naira, like 5000 or 5000.50";
} else {
const [naira, kobo = ""] = cleaned.split(".");
const amountKobo = Number(naira) * 100 + Number(kobo.padEnd(2, "0"));
if (amountKobo === 0) errors.amount = "Enter an amount above zero";
else if (amountKobo > dueKobo) errors.amount = "That's more than you owe on this invoice";
}
const ref = reference.trim();
if (ref === "") errors.reference = "Enter the reference from your bank transfer";
else if (!/^[A-Za-z0-9-]{6,30}$/.test(ref)) errors.reference = "References are 6 to 30 letters, numbers or dashes";
return errors;
}
const cases = [
{ amount: "", reference: "" },
{ amount: "5,000", reference: "TRF-2026-0915-0042" },
{ amount: "20000", reference: "abc" },
{ amount: "5000.555", reference: "TRF-2026-0915-0042" },
];
for (const c of cases) console.log(JSON.stringify(c), "->", JSON.stringify(validatePayment(c, 1783050)));{"amount":"","reference":""} -> {"amount":"Enter how much you're paying","reference":"Enter the reference from your bank transfer"}
{"amount":"5,000","reference":"TRF-2026-0915-0042"} -> {}
{"amount":"20000","reference":"abc"} -> {"amount":"That's more than you owe on this invoice","reference":"References are 6 to 30 letters, numbers or dashes"}
{"amount":"5000.555","reference":"TRF-2026-0915-0042"} -> {"amount":"Enter an amount in naira, like 5000 or 5000.50"}An empty object means valid. Now show the errors accessibly. On the page from lesson 6, add novalidate to the <form> and an empty error paragraph after each input (<p id="amount-error" class="error"></p> and <p id="reference-error" class="error"></p>), then:
HTML
<script>
// validatePayment() from the example goes here, unchanged.
const form = document.querySelector("#payment-form");
form.addEventListener("submit", (event) => {
event.preventDefault();
const values = { amount: form.amount.value, reference: form.reference.value };
const errors = validatePayment(values, DUE_KOBO);
for (const field of ["amount", "reference"]) {
const input = form[field];
const message = document.querySelector(`#${field}-error`);
message.textContent = errors[field] ?? "";
input.setAttribute("aria-invalid", errors[field] ? "true" : "false");
input.setAttribute("aria-describedby", errors[field] ? `${field}-error` : `${field}-hint`);
}
const firstError = ["amount", "reference"].find((field) => errors[field]);
if (firstError) form[firstError].focus();
else sendPayment(values); // lesson 8
});
</script>Submit the empty form: both messages appear under their fields in words, the fields are marked invalid, focus moves to the amount, and a screen reader reads the amount's error.
Walkthrough
- Run the example in your console with a case of your own.
- Add the script to
pay.htmland submit with each kind of mistake. - Bypass the page: in the console, call the API (lesson 8) with a negative amount. What protects Tallybook now?
- Write a validation rule for a new field (the task below).
Practice
Practice
How many error messages does validatePayment({ amount: "20000", reference: "abc" }, 1783050) return?
Task
6 minAdd a phone number field for a payment receipt by SMS. Write the JavaScript lines for validatePayment that check phone: optional, but if given it must be a Nigerian mobile number, 11 digits starting with 0 (spaces allowed), with a clear message.
Your work is checked for
- Removes spaces
- Optional: skips the check when empty
- Checks 11 digits starting with 0
- Sets errors.phone with a helpful message
Check your understanding
Answer every question to check.