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()andurl()skip an empty value (if (!value) return null) — they check format, and requiredness is added separately byrequired. So "optional email, but valid if present" is just[email()], and required is[required('Email'), email()].url()usesnew URL(value), not a regex. The browser parses URLs better than any pattern you'd write;try/catchcatches 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.
validateFieldskeeps 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.