Skip to main content
Technology & EngineeringForms Validation243 lines

Vest

Vest — unit-test-inspired form validation framework with suite/test syntax, async rules, group support, warning-level validations, and framework-agnostic design

Quick Summary16 lines
You are an expert in using Vest for form handling and validation.

## Key Points

- Use `only(currentField)` to run validation incrementally on the field the user is editing — this avoids showing errors on untouched fields and improves performance.
- Separate validation suites from component code. Define suites in their own files so they can be reused across client and server, or shared between frameworks.
- Use `warn()` for soft guidance like password strength or field recommendations, reserving hard errors for rules that must pass before submission.
- Forgetting to call `suite.reset()` when a form is unmounted or reset — stale state from the previous run will persist in the suite's internal cache and produce incorrect results.

## Quick Example

```bash
npm install vest
```
skilldb get forms-validation-skills/VestFull skill: 243 lines
Paste into your CLAUDE.md or agent config

Vest — Forms & Validation

You are an expert in using Vest for form handling and validation.

Core Philosophy

Overview

Vest is a validation framework that borrows its syntax from unit testing libraries like Mocha and Jest. Instead of defining schemas, you write validation "suites" containing individual "tests" for each field. This makes validation logic read like specifications — each test describes one rule, can be skipped conditionally, and reports failures with human-readable messages. Vest is framework-agnostic, works on both client and server, supports async validations, and provides built-in mechanisms for warning-level messages, field grouping, and stateful suite results that track which fields have been validated.

Setup & Configuration

npm install vest

Basic suite definition:

import { create, test, enforce } from "vest";

const suite = create((data = {}, currentField?: string) => {
  // Only validate the field that changed (optional optimization)
  only(currentField);

  test("username", "Username is required", () => {
    enforce(data.username).isNotBlank();
  });

  test("username", "Username must be at least 3 characters", () => {
    enforce(data.username).longerThanOrEquals(3);
  });

  test("email", "Email is required", () => {
    enforce(data.email).isNotBlank();
  });

  test("email", "Must be a valid email", () => {
    enforce(data.email).matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });

  test("password", "Password must be at least 8 characters", () => {
    enforce(data.password).longerThanOrEquals(8);
  });
});

Using with React:

import { useState } from "react";

function SignupForm() {
  const [formData, setFormData] = useState({ username: "", email: "", password: "" });
  const [result, setResult] = useState(suite.get());

  const handleChange = (field: string, value: string) => {
    const nextData = { ...formData, [field]: value };
    setFormData(nextData);
    // Run validation only for the changed field
    const res = suite(nextData, field);
    setResult(res);
  };

  return (
    <form>
      <input
        value={formData.username}
        onChange={(e) => handleChange("username", e.target.value)}
      />
      {result.getErrors("username").map((err) => (
        <span key={err} className="error">{err}</span>
      ))}
    </form>
  );
}

Core Patterns

The enforce API

Vest ships with enforce, a chainable assertion library similar to expect in testing:

import { enforce } from "vest";

// Built-in rules
enforce(value).isNotBlank();
enforce(value).isNumeric();
enforce(value).longerThan(5);
enforce(value).shorterThanOrEquals(100);
enforce(value).matches(/^[a-z]+$/);
enforce(value).inside(["admin", "user", "guest"]);

// Extend enforce with custom rules
enforce.extend({
  isValidPostalCode(value: string) {
    return { pass: /^\d{5}(-\d{4})?$/.test(value), message: "Invalid postal code" };
  },
});

enforce("90210").isValidPostalCode();

Warnings vs Errors

Vest supports warn() to mark tests as warnings instead of hard errors — useful for password strength meters or recommendations:

import { create, test, enforce, warn } from "vest";

const suite = create((data = {}) => {
  test("password", "Password must be at least 8 characters", () => {
    enforce(data.password).longerThanOrEquals(8);
  });

  test("password", "Consider adding a number for extra security", () => {
    warn(); // This test produces a warning, not an error
    enforce(data.password).matches(/\d/);
  });
});

const result = suite({ password: "abcdefgh" });
result.hasErrors("password");   // false
result.hasWarnings("password"); // true
result.getWarnings("password"); // ["Consider adding a number for extra security"]

Async Validation

import { create, test, enforce } from "vest";

const suite = create((data = {}) => {
  test("username", "Username is already taken", async () => {
    const response = await fetch(`/api/check-username?u=${data.username}`);
    const { available } = await response.json();
    if (!available) {
      throw new Error(); // Failing an async test
    }
  });
});

Groups

Group related tests together for conditional validation:

import { create, test, group, enforce } from "vest";

const suite = create((data = {}, currentField?: string) => {
  group("signupFields", () => {
    test("password", "Password is required", () => {
      enforce(data.password).isNotBlank();
    });

    test("confirmPassword", "Passwords must match", () => {
      enforce(data.confirmPassword).equals(data.password);
    });
  });

  group("profileFields", () => {
    test("bio", "Bio must be under 500 characters", () => {
      enforce(data.bio).shorterThanOrEquals(500);
    });
  });
});

const result = suite(formData);
result.hasErrorsByGroup("signupFields"); // check only signup-related errors

only and skip for Selective Validation

import { create, test, enforce, only, skip } from "vest";

const suite = create((data = {}, currentField?: string) => {
  only(currentField); // Only run tests for this field

  // Or skip specific fields
  // skip("confirmPassword");

  test("username", "Required", () => {
    enforce(data.username).isNotBlank();
  });

  test("email", "Required", () => {
    enforce(data.email).isNotBlank();
  });
});

// Only validates "username", skips everything else
suite({ username: "alice" }, "username");

Suite Result API

const result = suite(formData);

result.isValid();             // true if no errors in entire suite
result.isValid("email");      // true if no errors for specific field
result.hasErrors();           // any errors at all?
result.hasErrors("email");    // errors for a specific field?
result.getErrors("email");    // string[] of error messages
result.hasWarnings("password");
result.getWarnings("password");
result.done((res) => {        // called when all async tests finish
  console.log(res);
});

Best Practices

  • Use only(currentField) to run validation incrementally on the field the user is editing — this avoids showing errors on untouched fields and improves performance.
  • Separate validation suites from component code. Define suites in their own files so they can be reused across client and server, or shared between frameworks.
  • Use warn() for soft guidance like password strength or field recommendations, reserving hard errors for rules that must pass before submission.

Common Pitfalls

  • Forgetting to call suite.reset() when a form is unmounted or reset — stale state from the previous run will persist in the suite's internal cache and produce incorrect results.
  • Treating async test results synchronously. Async tests return a pending result initially; you must use result.done(callback) or await the suite promise to get final results that include async validations.

Anti-Patterns

Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.

Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.

Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add forms-validation-skills

Get CLI access →