This episode covers forms from two sides: built-in HTML5 validation that's fast, and custom JavaScript validation for more complex rules. You also learn to display clear error messages, validate as you type, and process submission asynchronously.

Forms are the main gateway for a web application to receive data from users — and also the biggest source of dirty data. Users can submit empty strings, emails without @, or numbers where text belongs. Validation ensures incoming data follows the rules, both before it reaches the server and before it's processed.
Episode 17 covers layered validation. The first layer is HTML5 validation, built into the browser — fast and with no JavaScript code. The second layer is custom JavaScript validation for rules HTML can't express. Finally, you build good feedback: clear error messages, validation as you type, and submission processed asynchronously.
The principle to hold: client-side validation for user experience, server-side validation for security. Both are required — neither replaces the other.
Start with the correct HTML structure. The key elements: a form, labels connected to inputs, and appropriate input types:
<form id="form-daftar" novalidate>
<label for="nama">Nama</label>
<input type="text" id="nama" name="nama" required minlength="3" />
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
<button type="submit">Daftar</button>
<p id="pesan-error" hidden></p>
</form><input type="email" tells the browser this field must be email-formatted. required marks a mandatory field, minlength="3" limits the minimum length. The novalidate attribute on the form is deliberately turned on — so you can apply JavaScript validation with full control. The p element with id pesan-error will hold the feedback.
Even without novalidate, the browser has built-in HTML5 validation. When validation fails, the invalid event and the validity property give detailed information:
const inputEmail = document.querySelector("#email");
inputEmail.addEventListener("input", () => {
console.log("valid:", inputEmail.validity.valid);
console.log("typeMismatch:", inputEmail.validity.typeMismatch);
console.log("valueMissing:", inputEmail.validity.valueMissing);
});inputEmail.validity.valid is true when the input passes all HTML5 rules. validity.typeMismatch flags a wrong format, validity.valueMissing flags an empty required field. When a form tries to submit and something fails, the invalid event fires and validationMessage holds the browser's built-in message — which you can display yourself without the default popup.
Rules like "password must contain a number" or "confirmation must match the password" can't be expressed with HTML attributes. That's JavaScript's job:
function validasiPassword(kataSandi, konfirmasi) {
if (kataSandi.length < 8) {
return "Password minimal 8 karakter";
}
if (!/[0-9]/.test(kataSandi)) {
return "Password harus mengandung angka";
}
if (kataSandi !== konfirmasi) {
return "Konfirmasi password tidak cocok";
}
return null;
}
console.log(validasiPassword("rahasia", "rahasia"));
console.log(validasiPassword("rahasia12", "rahasia12"));validasiPassword returns an error message string, or null if valid. Separating rules into pure functions like this makes them easy to test. Using null as the "valid" marker is a clear, easy-to-check convention.
The best error messages appear near the problematic input, not piled up in one place:
function tampilkanError(input, pesan) {
const errorEl = input.nextElementSibling;
if (pesan === null) {
errorEl.hidden = true;
input.classList.remove("salah");
} else {
errorEl.textContent = pesan;
errorEl.hidden = false;
input.classList.add("salah");
}
}
const inputNama = document.querySelector("#nama");
tampilkanError(inputNama, validasiNama(inputNama.value));tampilkanError uses input.nextElementSibling — assuming the error element sits right after the input in the HTML. When valid, the message is hidden and the salah class is removed; when not, the message shows and the input is marked red. Consistent error placement means users don't have to hunt around.
Validation that only happens on submit feels sluggish. A common pattern: light validation during input, full validation on blur and submit:
const inputNama = document.querySelector("#nama");
const form = document.querySelector("#form-daftar");
inputNama.addEventListener("input", () => {
const pesan = inputNama.value.length < 3 ? "Nama minimal 3 karakter" : null;
tampilkanError(inputNama, pesan);
});
form.addEventListener("submit", (event) => {
event.preventDefault();
const pesan = validasiNama(inputNama.value);
tampilkanError(inputNama, pesan);
if (pesan === null) {
console.log("Form valid, siap dikirim");
}
});inputNama.addEventListener("input", ...) updates the message on every keystroke. On submit, event.preventDefault() stops the default submission, final validation runs, and only if clean is the data processed. Users get instant feedback without waiting for submit.
If all fields are valid, send the data to the server with fetch and display the result:
async function kirimPendaftaran(data) {
const respons = await fetch("/api/daftar", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!respons.ok) {
throw new Error(`HTTP ${respons.status}`);
}
return respons.json();
}
const dataForm = {
nama: document.querySelector("#nama").value,
email: document.querySelector("#email").value,
};
await kirimPendaftaran(dataForm);
console.log("Pendaftaran berhasil");async function kirimPendaftaran(data) uses the fetch POST pattern from episode 15, complete with the respons.ok check. Separating the send function from the form logic makes the code easy to test, and await errors are caught with try...catch as in episode 16.
Episode 17 enabled you to build reliable forms: HTML5 validation with the Constraint Validation API, custom validation for complex rules, clear error messages shown near inputs, validation while typing, and asynchronous submission with fetch.
Key takeaways:
validity and validationMessage give programmatic access to browser validation.novalidate gives full control to JavaScript validation.In the next episode 18 we'll cover Web Storage, cookies, and client-side state — storing data in the browser with localStorage and sessionStorage, working with cookies, and safe, effective state patterns. You'll know when to use which.