Vest
Vest — unit-test-inspired form validation framework with suite/test syntax, async rules, group support, warning-level validations, and framework-agnostic design
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 linesVest — 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
Related Skills
Conform
Conform — progressive enhancement forms, Server Actions, Zod integration, nested objects, arrays, intent buttons, and accessibility patterns
Formik
Formik — useFormik, Field, Form, validation with Yup, FieldArray, custom inputs, submission handling, and error display patterns
React Final Form
React Final Form — subscription-based form state management with fine-grained re-renders, field-level validation, decorator support, and zero dependencies
React Hook Form
React Hook Form — useForm, register, Controller, validation, nested fields, arrays (useFieldArray), DevTools, and performance optimization patterns
Valibot
Valibot — modular schema validation, tiny bundle size, parse/safeParse, pipe transformations, type inference, and comparison with Zod
Yup
Yup — schema-based object validation with casting, transforms, conditional rules, localized error messages, and deep Formik integration