Form Validation EngineLoading saved progress…

Form Validation Engine

A form validation engine is the logic core of a form library: you declare a schema of per-field rules, hand it the form's values, and it tells you which fields are invalid and why. It is the piece that libraries like Yup, Zod, and react-hook-form resolvers wrap in a friendlier API. You will build a small one, exposed as a factory formValidationEngine(schema), and it must collect every error — not stop at the first — and cope with rules that answer asynchronously, like a "username already taken?" check against a server.

Signature

const engine = formValidationEngine(schema);
//   schema: { [field]: Rule[] }   each field maps to a list of rules

// A rule inspects one field's value (and, if it needs it, the whole form)
// and returns a message when invalid, or nothing when valid. It may be async.
type Rule = (value, allValues) => string | undefined | Promise<string | undefined>;

engine.validate(values);
//   -> Promise<{ valid: boolean, errors: { [field]: string[] } }>
//   collects ALL failing messages per field; passing fields are absent

engine.validateField(name, values); // -> Promise<string[]>  one field's messages

// Built-in rule creators (each returns a Rule):
required(message?);        // present and not blank
minLength(n, message?);    // at least n characters
pattern(regex, message?);  // matches a regular expression

Examples

const engine = formValidationEngine({
  name: [required('Name is required')],
  password: [minLength(8, 'Too short'), pattern(/[0-9]/, 'Needs a digit')],
});

await engine.validate({ name: '', password: 'abc' });
// {
//   valid: false,
//   errors: {
//     name: ['Name is required'],
//     password: ['Too short', 'Needs a digit'], // BOTH failures, in order
//   },
// }
// A cross-field rule reads another field; an async rule checks the server.
const taken = ['ada'];
const engine = formValidationEngine({
  username: [async (value) => (taken.includes(value) ? 'Taken' : undefined)],
  confirm: [(value, all) => (value === all.password ? undefined : 'Must match')],
});

await engine.validate({ username: 'grace', password: 'x', confirm: 'x' });
// { valid: true, errors: {} }   nothing failed, so errors is empty

Notes

  • A rule is just a function — it returns a message string to fail and a falsy value (return nothing) to pass. Rules compose because a field is an array of them; the engine runs the whole array.
  • Collect all errors — gather every failing rule for a field into an array in rule-declared order, not just the first. A field with no failures does not appear in errors at all.
  • Cross-field rules — every rule receives the whole values object as its second argument, so a confirmPassword rule can compare against values.password.
  • Async rules are awaited — a rule may return a promise, so validate returns a promise that resolves only after every rule, sync and async, has settled. Run them concurrently.
  • valid follows the errors — it is true exactly when no field has any message.
  • Do not worry about — debouncing the async checks, nested or array schemas, i18n of messages, or wiring this to real DOM inputs. The rule contract and the two methods above are the whole surface.

FAQ

What is a validation rule in this engine?
A rule is a plain function that takes the field value and all the form's values and returns a message string when the value is invalid, or undefined when it passes. Rules compose because a field is just an array of them, and any rule may be async by returning a promise of the same.
Does the engine stop at the first error or collect them all?
It collects every failing rule for every field. Each field maps to an array of messages in rule-declared order, and a field with no failures is left out of the errors map entirely. This is what lets a form show a user every problem at once instead of one at a time.
How do async validators fit in?
A rule may return a promise, so the engine awaits every rule's result before assembling the errors. Rules run concurrently via Promise.all, so a slow username-taken check and the sync rules all overlap, and validate resolves only once every rule has settled.
Loading editor…