Code

js/validation.js — pure rules

In the code column — js/validation.js: rules as pure functions. Each takes a value and returns an error string or null.

Notice the pattern — a rule is a function that returns a function:

export const minLength = (n, label = 'This field') => (value) =>
  value && value.length < n ? `${label} must be at least ${n} characters.` : null;

minLength(3, 'Username') returns a rule ready for a value. That's why they compose into an array, and validateFields runs them all and returns the first error per field:

validateFields(
  { email: 'nope', name: '' },
  { email: [required('Email'), email()], name: [required('Name')] }
);
// → { email: 'Please enter a valid email address.', name: 'Name is required.' }

Two small details that say a lot:

  • email() and url() skip an empty value (if (!value) return null) — they check format, and requiredness is added separately by required. So "optional email, but valid if present" is just [email()], and required is [required('Email'), email()].
  • url() uses new URL(value), not a regex. The browser parses URLs better than any pattern you'd write; try/catch catches the malformed ones.

These functions are fully decoupled from the DOM. You can test them without a browser (validateFields(...) returns an object). That's exactly why the validation "brain" is kept separate from the form "body" (the next step).

Tip. validateFields keeps only the first error per field (break). It's better for the user to see "Email is required", fix it, then "Email is invalid" — one at a time — than the whole list at once.