Vest Form Validation Framework
Vest is a declarative and framework-agnostic JavaScript validation framework that enables developers to write validation logic using a syntax familiar to unit testing frameworks like Jest or Mocha. It aims to separate validation concerns from UI logic, promoting cleaner components and easier testing. Currently stable at version 6.3.2, Vest offers robust features including asynchronous validation support, strong TypeScript type safety, server-side rendering (SSR) compatibility with state hydration, and extensibility for custom rules. It distinguishes itself by managing validation state intelligently, handling dependent fields, and providing a powerful assertion library (`enforce`). The project maintains a consistent release cadence with regular patch and minor updates, and significant architectural shifts between major versions. Vest is designed for complex validation scenarios across various JavaScript environments (React, Vue, Svelte, Angular, Node.js, vanilla JS) and implements the Standard Schema specification.
Common errors
-
TypeError: suite is not a function
cause Attempting to call the result of `create` directly as a function, which was the behavior in Vest v5, but `create` returns a Suite Object in v6.fixCall the `.run()` method on the suite object: `const result = mySuite.run(data);`. -
ReferenceError: test is not defined
cause `test` was not correctly imported as a named export. This often happens if attempting a default import (e.g., `import vest from 'vest'`) or if `test` is simply missing from the named import.fixEnsure `test` is imported as a named export: `import { create, test, enforce } from 'vest';`. -
Cannot read properties of undefined (reading 'isNotBlank')
cause The `enforce` assertion was called with an `undefined` value, usually when `data.fieldName` is missing or `null` in the input, and Vest attempts to apply an assertion that expects a defined value.fixEnsure the field exists or add conditional checks before enforcing: `if (data.username) { enforce(data.username).isNotBlank(); }` or handle `undefined` within your schema definition. -
Asynchronous validation is not waiting for completion or result is premature.
cause Forgetting to `await` the result of `suite.run()` when the suite contains asynchronous tests, or not handling the Promise-like nature of the v6 `SuiteResult` object.fixAlways `await` the result when working with async validations: `const result = await mySuite.run(data);`.
Warnings
- breaking In Vest v6, `create` now returns a `Suite Object` with methods like `.run()`, `.reset()`, and `.get()`, instead of directly returning a callable function. Direct calls to the suite function (e.g., `suite(data)`) will result in a TypeError.
- breaking The `promisify` utility and the `.done()` callback on the result object were removed in Vest v6. The `suite.run()` method now returns a Promise-like object, simplifying asynchronous handling.
- breaking The `test.memo` API was promoted to a top-level `memo` export in Vest v6 and must be imported from `vest/memo`. It now wraps a block of logic, not just individual tests.
- breaking Vest v4 removed default import support (e.g., `import vest from 'vest'`) for better tree-shaking. All core Vest utilities like `create`, `test`, and `enforce` must be explicitly named imports.
- gotcha Using standard `if/else` statements to conditionally run tests within a Vest suite can lead to unpredictable behavior and out-of-sync test results, as Vest relies on the consistent order of execution for test state management.
- gotcha In Vest v6, focusing or skipping specific fields or groups (e.g., `only`, `skip`) is handled by methods on the `Suite Object` (e.g., `suite.only('fieldName').run(data)`) rather than by passing arguments to `only()` or `skip()` inside the suite callback.
Install
-
npm install vest -
yarn add vest -
pnpm add vest
Imports
- create
const create = require('vest');import { create } from 'vest'; - test
import vest, { test } from 'vest';import { test } from 'vest'; - enforce
const { enforce } = require('vest');import { enforce } from 'vest'; - memo
import { memo } from 'vest/memo'; - SuiteSerializer
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
Quickstart
import { create, test, enforce } from 'vest';
// Simulate an async API call, e.g., checking if a username exists
const doesUsernameExist = async (username: string): Promise<boolean> => {
return new Promise(resolve => {
setTimeout(() => {
resolve(username === 'admin'); // 'admin' is taken
}, 500);
});
};
interface FormData {
username?: string;
email?: string;
password?: string;
confirmPassword?: string;
}
const signupSuite = create((data: FormData = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
test('username', 'Username must be at least 3 characters long', () => {
enforce(data.username).longerThanOrEquals(3);
});
// Async validation
test('username', 'Username is already taken', async () => {
if (data.username) {
await enforce(await doesUsernameExist(data.username)).isFalsy();
}
});
test('email', 'Email is required', () => {
enforce(data.email).isNotBlank();
});
test('email', 'Email must be valid', () => {
enforce(data.email).isEmail();
});
test('password', 'Password is required', () => {
enforce(data.password).isNotBlank();
});
test('password', 'Password must be at least 8 characters long', () => {
enforce(data.password).longerThanOrEquals(8);
});
// Conditional validation: only run if password is valid and present
if (data.password && signupSuite.get().hasErrors('password') === false) {
test('confirmPassword', 'Passwords do not match', () => {
enforce(data.confirmPassword).equals(data.password);
});
}
});
async function validateForm(formData: FormData) {
console.log('Validating with data:', formData);
const result = await signupSuite.run(formData);
console.log('Validation Results:');
console.log(' isValid:', result.isValid());
console.log(' hasErrors:', result.hasErrors());
console.log(' getErrors:', result.getErrors());
console.log(' getWarnings:', result.getWarnings());
if (!result.isValid()) {
console.log('Validation failed!');
} else {
console.log('Validation passed!');
}
}
// Example usage
validateForm({ username: '', email: 'test@example.com', password: 'password123', confirmPassword: 'password123' });
validateForm({ username: 'john', email: 'john@example.com', password: 'short', confirmPassword: 'short' });
validateForm({ username: 'admin', email: 'admin@example.com', password: 'password123', confirmPassword: 'password123' });
validateForm({ username: 'newUser', email: 'new@example.com', password: 'supersecurepassword', confirmPassword: 'supersecurepassword' });