# Vest 6 Documentation
# API Reference
Below is a list of all the API functions exposed by Vest.
## Vest's main export API
### `create(callback, schema?)`
Creates a new validation suite. Returns a **Suite Object**.
- [Read more about `create`](https://vestjs.dev/docs/writing_your_suite/vests_suite)
- [Read more about Schema Validation](https://vestjs.dev/docs/writing_your_suite/schema_validation)
- `callback`: The validation logic.
- `schema` (Optional): An `enforce` schema definition.
### Suite Object Methods
#### `suite.run(...args)`
Runs the suite. Passes arguments to the suite callback.
- **Returns**: A `SuiteResult` object.
- If the suite contains async tests, the result object **also implements the Promise interface**, allowing you to `await` it.
- You can always access synchronous result data immediately (e.g., `result.hasErrors()`), even if the promise is pending.
- [Read more about `suite.run`](https://vestjs.dev/docs/writing_your_suite/vests_suite#running-validations)
#### `suite.runStatic(...args)`
Runs the suite in stateless mode. Useful for server-side validation.
- [Read more about Server Side Validation](https://vestjs.dev/docs/server_side_validations)
#### `suite.reset()`
Resets the suite state (clears all results).
- [Read more about `suite.reset`](https://vestjs.dev/docs/writing_your_suite/vests_suite#resetting-the-suite)
#### `suite.remove(fieldName)`
Removes a specific field from the suite result state.
- [Read more about `suite.remove`](https://vestjs.dev/docs/writing_your_suite/vests_suite#removing-a-field)
#### `suite.resetField(fieldName)`
Resets the state of a specific field (clears errors/warnings but keeps it in the result).
- [Read more about `suite.resetField`](https://vestjs.dev/docs/writing_your_suite/vests_suite#resetting-a-single-field)
#### `suite.focus(config)`
Prepares a focused run with combined modifiers. Use this when you need to combine `only`, `skip`, `skipGroup`, or `onlyGroup` in a single call.
- `config`: `{ only?: string | string[], skip?: string | string[], skipGroup?: string | string[], onlyGroup?: string | string[] }`
- [Read more about Focused Updates](https://vestjs.dev/docs/writing_your_suite/focused_updates)
#### `suite.only(fieldName)`
Shorthand for `suite.focus({ only: fieldName })`. Restricts the next run to the specified field(s).
- `fieldName`: `string | string[]`
- Returns a chainable suite with `run`, `afterEach`, `afterField`, `focus`, and `only`.
- [Read more about Focused Updates](https://vestjs.dev/docs/writing_your_suite/focused_updates#running-only-specific-fields)
#### `suite.afterEach(callback)`
Registers a callback to run after each test completes (including the initial sync run and every async completion). The callback receives **no arguments**; you should access the result using `suite.get()`.
- [Read more about `suite.afterEach`](https://vestjs.dev/docs/writing_your_suite/handling_completion#2-using-suiteaftereachcallback)
#### `suite.afterField(fieldName, callback)`
Registers a callback to run when a specific field finishes execution. The callback receives **no arguments**; you should access the result using `suite.get()`.
- [Read more about `suite.afterField`](https://vestjs.dev/docs/writing_your_suite/handling_completion#3-using-suiteafterfieldfieldname-callback)
#### `suite.get()`
Returns the current result object of the suite without running it. Useful for accessing the state inside UI components or subscribers.
- [Read more about `suite.get`](https://vestjs.dev/docs/writing_your_suite/vests_suite#accessing-results-without-running)
#### `SuiteSerializer.serialize(result)`
Returns a minified, serialized representation of a validation result. Useful for SSR hydration.
- [Read more about SSR Hydration](https://vestjs.dev/docs/server_side_validations#ssr--hydration)
#### `SuiteSerializer.resume(suite, data)`
Hydrates the suite with a serialized state.
- `suite`: The suite to resume.
- `data`: The serialized state string.
- [Read more about SSR Hydration](https://vestjs.dev/docs/server_side_validations#ssr--hydration)
#### `suite['~standard'].validate(data)`
Implements the [Standard Schema](https://standardschema.dev/) interoperability contract. Compatible consumers invoke this hook automatically. For application code, use the primary `suite.run(data)` stateful API or `suite.runStatic(data)` stateless API.
- [Read more about Standard Schema Support](https://vestjs.dev/docs/community_resources/standard_schema)
### Top-Level Exports
#### `enforce.context()`
Retrieves the current validation context during a suite run. Useful within custom rules to access other fields in the data object.
- **Returns**: `{ data: Object, value: any, ... }`
- [Read more about Context Aware Rules](https://vestjs.dev/docs/enforce/creating_custom_rules#context-aware-rules)
#### `enforce.extend(customRules)`
Extends Vest's enforce with custom validation rules.
- **Tip**: To add TypeScript support for your custom rules, see [TypeScript Support](https://vestjs.dev/docs/typescript_support#custom-enforce-rules).
#### `memo(callback, deps, options?)`
Memoizes a block of tests.
- `callback`: The function to execute if dependencies change.
- `deps`: Array of dependencies determining if the callback executes.
- `options` (Optional): Configuration object with `cacheSize` (max cached results) and `ttl` (Time-to-Live in ms).
- [Read more about `memo`](https://vestjs.dev/docs/writing_tests/advanced_test_features/memo)
#### `compose(...rules)`
Combines multiple enforce rules.
- [Read more about `compose`](https://vestjs.dev/docs/enforce/composing_enforce_rules)
#### `test(fieldName, message, callback)`
A single validation test inside your suite.
- [Read more about `test`](https://vestjs.dev/docs/writing_tests/the_test_function)
#### `enforce(value)`
Asserts that a value matches your desired result.
- [Read more about `enforce`](https://vestjs.dev/docs/enforce/enforce)
#### `warn()`
Sets the test's severity to warning in the synchronous part of a test.
- [Read more about `warn`](https://vestjs.dev/docs/writing_tests/warn_only_tests)
#### `useWarn()`
Returns a setter function that marks the current test as warning severity, including async flows after an `await`.
- [Read more about `useWarn`](https://vestjs.dev/docs/writing_tests/warn_only_tests)
#### `only(fieldName)`
Makes Vest only run the provided field names.
- [Read more about `only`](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/skip_and_only#only-running-specific-fields)
#### `skip(fieldName)`
Makes Vest skip the provided field names.
- [Read more about `skip`](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/skip_and_only#skipping-fields)
#### `include(fieldName).when(condition)`
Link fields by running them together based on a criteria.
- [Read more about `include`](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/include)
#### `skipWhen(condition, callback)`
Skips a portion of the suite when the provided condition is met.
- [Read more about `skipWhen`](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/skipWhen)
#### `omitWhen(condition, callback)`
Omits a portion of the suite when the provided condition is met.
- [Read more about `omitWhen`](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/omitWhen)
#### `optional(fieldName)`
Allows you to mark a field as optional.
- [Read more about `optional`](https://vestjs.dev/docs/writing_your_suite/optional_fields)
#### `group(groupName, callback)`
Allows grouping multiple tests with a given name.
- [Read more about `group`](https://vestjs.dev/docs/writing_tests/advanced_test_features/grouping_tests)
#### `each(list, callback)`
Allows iteration over an array of values to dynamically run tests.
- [Read more about `each`](https://vestjs.dev/docs/writing_tests/advanced_test_features/dynamic_tests)
#### `mode(mode)`
Determines whether Vest should continue running tests after a field has failed.
- [Read more about `mode`](https://vestjs.dev/docs/writing_your_suite/execution_modes)
## Suite Result API
After running your suite, the results object is returned. It has the following functions:
- [Read more about the Result Object](https://vestjs.dev/docs/writing_your_suite/accessing_the_result)
- `hasErrors(fieldName?)`: Returns true if the suite or the provided field has errors.
- `hasWarnings(fieldName?)`: Returns true if the suite or the provided field has warnings.
- `getError(fieldName?)`: Without a field, returns the first summary object (`{ fieldName, message, groupName }`). With a field, returns that field's first error message string. Returns `undefined` when none exists.
- `getWarning(fieldName?)`: Without a field, returns the first summary object. With a field, returns that field's first warning message string. Returns `undefined` when none exists.
- `getMessage(fieldName)`: Returns the field's first error or warning message string, preferring an error when both exist.
- `getErrors(fieldName?)`: Without a field, returns an object whose keys are field names and values are arrays of message strings. With a field, returns that field's array of error message strings.
- `getWarnings(fieldName?)`: Without a field, returns an object whose keys are field names and values are arrays of message strings. With a field, returns that field's array of warning message strings.
- `hasErrorsByGroup(groupName)`: Returns true if the provided group has errors.
- `hasWarningByGroup(groupName)`: Returns true if the provided group has warnings.
- `getErrorsByGroup(groupName)`: Returns an object with errors in the provided group.
- `getWarningsByGroup(groupName)`: Returns an object with warnings in the provided group.
- `isPending(fieldName?)`: Returns true if the suite has pending async tests.
- `isTested(fieldName)`: Returns true if the provided field has been tested.
- `isValid(fieldName?)`: Returns true if the suite or the provided field is valid.
- `isValidByGroup(groupName)`: Returns true if a certain group or a field in a group is valid or not.
- `value`: The parsed schema output when the suite is valid. Typed as the schema's output type. `undefined` when invalid or when no schema is used.
- `types`: When a schema is present, an object with `input` and `output` properties typed from the schema. `undefined` when no schema is used.
- `run`: Metadata about the latest run, including `run.data.raw` (current run data), `run.data.parsed` (parsed data for the current run), and `run.time` (timestamp).
# Custom Integrations
## ngx-vest-forms - Angular
[ngx-vest-forms](https://github.com/simplifiedcourses/ngx-vest-forms) -
A very lightweight adapter for Angular template-driven forms and Vest. This package gives us the ability to create unidirectional forms without any boilerplate. It is meant for complex forms with a high focus on complex validations and conditionals.
## React-Hook-Form Vest resolver
[React Hook Form](https://react-hook-form.com/api/useform/#validationResolver) - One of the most popular libraries for form in React, has a resolver for integration with Vest.
## Felte/Vest
[felte](https://felte.dev/docs/svelte/validators#using-vest) - An extensible form library for Svelte and Solid. The library has a pre-built integration with Vest.
## Ember-Vest
[Ember-Vest](https://antonbavykin1991.github.io/ember-vest/) - An Ember library that integrates Vest with Ember.js ([antonbavykin1991](https://github.com/antonbavykin1991))
# Showcase by Community Members
This is the place for the community to show off some of their custom Vest and Enforce use cases and solutions. Feel free to submit a pull request to add your own.
- [Simple Laravel Enforcer](https://gist.github.com/Elliot-Alexander/c1c05b56df155c4010e996a4aa8e0201) - A simple custom enforcer for using Vest to serve Laravel error responses within forms.
- [Vue `useSuite` Composable](https://gist.github.com/HappyTiptoe/b798357efc7c6bec59af01a5fa252f99) - A simple Vue composable for interacting with Vest validation suites.
# Standard Schema Support
Vest suites and Enforce rules implement [Standard Schema V1](https://standardschema.dev/). This gives ecosystem tools a vendor-neutral way to call Vest without a Vest-specific adapter.
## React Hook Form
The official React Hook Form resolver package includes a Standard Schema resolver:
```shell
npm install vest react-hook-form @hookform/resolvers
```
```tsx
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { useForm } from 'react-hook-form';
import { create, enforce, test } from 'vest';
const suite = create(data => {
test('email', 'Enter a valid email address', () => {
enforce(data.email).isEmail();
});
});
function EmailForm() {
const form = useForm({
resolver: standardSchemaResolver(suite),
});
return (
);
}
```
The resolver is the simplest option when the form manager should validate the whole form. To validate one field during interaction, run that field through the same suite:
```ts
suite.only('email').run(form.getValues());
```
See the tested [production architecture](https://vestjs.dev/docs/guides/production-architecture) for focused async validation, warnings, conditionals, a replaceable Zod boundary, and a stateless server run. An Enforce schema can own the boundary instead.
## Direct Standard Schema validation
Most application code should pass the suite to a compatible consumer and let that tool invoke the Standard Schema contract. If you are implementing or testing an integration directly, call the contract explicitly:
```ts
const output = await suite['~standard'].validate(data);
if (output.issues) {
console.log(output.issues);
} else {
console.log(output.value);
}
```
For normal Vest application code, use `suite.run()` for stateful execution or `suite.runStatic()` for an independent server run. Those are the primary APIs and expose Vest selectors such as `isPending`, `isTested`, `getWarnings`, and group status. `.validate()` exists only for Standard Schema interoperability.
## Enforce rules
Enforce schemas also expose the Standard Schema interface, making them suitable for parsing-oriented integrations that do not need Vest's stateful suite runtime.
Read [Vest, schema validators, and form libraries](https://vestjs.dev/docs/vest_vs_the_rest) for guidance on assigning responsibilities between these layers.
# Tutorials and learning resources
- [Advanced Template-driven forms course](https://www.simplified.courses/complex-angular-template-driven-forms) Stop writing boilerplate code in Angular forms, Angular course by [@brechtbilliet](https://twitter.com/brechtbilliet).
- [Angular + Vest](https://www.youtube.com/watch?v=EMUAtQlh9Ko) = "Form validation Done Right" by Ward Bell in ng-cof 2022. [Github repo](https://github.com/wardbell/ngc-validate).
- [AgnosticUI + Vest](https://developtodesign.com/agnosticui-examples) - Demo form using Svelte package of [AgnosticUI](https://agnosticui.com/) - a UI component library that works with React, Vue 3, and Svelte - with Vest for form validation.
- [Svelte Forms: The Missing Manual](https://codechips.gumroad.com/l/svelte-forms) - An excellent book by [Ilia Mikhailov](https://twitter.com/codechips). The book contains several chapters of integration examples with Vest.
- [Vue Form Validations With Vest (video)](https://portal.gitnation.org/contents/vue-form-validations-with-vest) - A Vue.JS London presentation on how to use Vest with Vue.
- [Up your form validation game with Vest (video)](http://www.youtube.com/watch?v=X2PuiawaGV4) - A session from the Svelte Summit on how to use Vest with Svelte.
# How Vest Handles Validation
Most validators inspect a value and return an answer. Vest can do that too, but a suite can also keep its result between runs.
A schema validator usually answers:
> Does this value match the required structure right now?
An interactive form has a few more questions:
> What changed? Which rules need to run again? Can the other results stay as they are? Is this async response still current? Can the user continue?
Those questions come up in forms, onboarding, checkout flows, settings screens, and anywhere validation happens a little at a time.
## What a suite gives you
### 1. Executable business rules
Vest treats validations as named tests:
```js
import { create, enforce, test } from 'vest';
const suite = create(data => {
test('password', 'Password is required', () => {
enforce(data.password).isNotBlank();
});
test('password', 'Password must contain a number', () => {
enforce(data.password).matches(/\d/);
});
});
```
The familiar syntax is valuable because it gives validation logic a consistent structure. Rules live outside UI components, support multiple tests per field, and can be unit-tested without simulating DOM events.
### 2. One result that updates over time
A suite keeps the latest result for every test it has seen.
When `suite.run()` executes, Vest:
1. runs the relevant tests;
2. records passing, failing, warning, and pending outcomes;
3. reconciles them with previous runs;
4. ignores results that are no longer relevant;
5. returns the complete current validation picture.
```text
Password changes
→ run password tests
→ retain the earlier email result
Username changes
→ run username tests
→ retain password and email
→ ignore an older username request if it finishes late
```
The result is still complete even when the latest run only checked one field.
### 3. Assertions, schemas, and integration
`enforce` provides assertions, schemas, parsing, and custom rules. Vest suites and Enforce rules also implement Standard Schema for interoperability with compatible tools.
The schema checks and parses the input. The suite then keeps track of test results as the user interacts with the form.
## Validation state, not form state
Vest deliberately does not own:
- input values;
- DOM nodes;
- field registration;
- UI events;
- submission transport.
It owns:
- which validation tests ran;
- which passed, failed, warned, or are pending;
- which fields and groups are valid;
- which results were retained, skipped, omitted, or canceled;
- whether the workflow as a whole is ready.
This makes Vest compatible with local component state, React Hook Form, Formik, server actions, custom state machines, or no form library at all.
## Stateful and stateless execution
| Mode | Method | Best for |
| ------------- | ------------------- | ---------------------------------------- |
| **Stateful** | `suite.run()` | Interactive forms and progressive flows |
| **Stateless** | `suite.runStatic()` | API requests and independent server runs |
Stateful runs merge new results into the suite. Stateless runs start fresh and discard state after producing the result.
For SSR applications, Vest can serialize a stateless server result and resume it in a stateful browser suite. That preserves more context than sending an error object alone.
## Focused execution
```js
suite.only('username').run(data);
suite.focus({ onlyGroup: 'shipping' }).run(data);
suite.focus({ skip: 'promoCode', skipGroup: 'billing' }).run(data);
```
Focused runs apply to the next execution only. Fields that do not run retain their previous results.
This improves:
- **performance** by avoiding irrelevant work;
- **user experience** by not exposing errors for untouched fields;
- **correctness** by preserving the full validation picture;
- **cost** by not repeating heavy computations or server requests.
## Async validation is a concurrency problem
Supporting promises is not enough for interactive validation. Requests can overlap and finish in a different order than they started.
Vest associates async tests with their validation run. When a field runs again, older work is canceled and its eventual result cannot replace the newer state. Tests receive an `AbortSignal` so network operations can stop early when possible.
The result exposes pending state immediately:
```js
const result = suite.only('username').run(data);
result.isPending('username'); // available immediately
await result; // wait for the current run to finish
```
## Dependent and conditional workflows
Real workflows are rarely independent field maps. Vest includes primitives for relationships and changing structure:
- `include(field).when(...)` reruns dependent fields;
- `group()` represents steps or logical sections;
- `skipWhen()` avoids work while retaining validity requirements;
- `omitWhen()` removes currently irrelevant tests from validity;
- `optional()` represents values that may be absent;
- `each()` tracks dynamic list items with stable keys;
- warnings provide guidance without blocking completion.
These tools describe relationships between tests, not just the shape of a value.
## Errors are not the same as incompleteness
A suite may not be valid yet because:
- a required field has not been tested;
- an async test is pending;
- a required section has not been reached.
That is different from an active validation error. Vest exposes `isTested`, `isPending`, errors, and warnings separately so the UI can respond without treating an unfinished user as a failing user.
## Framework independence
Because suites do not depend on UI components, the same validation contract can be used with React, Vue, Svelte, Angular, vanilla JavaScript, or Node.js.
Your framework decides when to run the suite and how to show its result. Vest keeps track of the validation itself.
## In short
Vest's test-like syntax is familiar, but the state kept by the suite is the important part.
It lets you validate what changed without forgetting what already passed, and it prevents an old async response from overwriting a newer result.
Continue with [Understanding Vest's State](https://vestjs.dev/docs/understanding_state) or see [Vest alongside schema and form libraries](https://vestjs.dev/docs/vest_vs_the_rest).
# Compound rules
Alongside the list of rules that only accept data provided by the user, enforce also supports compound rules - these are rules that accept other rules as their arguments. These rules let you validate more complex scenarios with the ergonomics of enforce.
To use it, simply use them in your project.
These rules are available in `enforce`:
- [enforce.anyOf() - or validations](#enforceanyof---or-validations)
- [enforce.allOf() - all/and validations](#enforceallof---alland-validations)
- [enforce.oneOf()](#enforceoneof)
- [enforce.noneOf - None rules](#enforcenoneof---none-rules)
## enforce.anyOf() - or validations
Sometimes a value has more than one valid possibility, `any` lets us validate that a value passes _at least_ one of the supplied rules.
```js
enforce(value).anyOf(enforce.isString(), enforce.isArray());
// A valid value would either an array or a string.
```
## enforce.allOf() - all/and validations
`allOf` lets us validate that a value passes _all_ of the supplied rules.
```js
enforce(value).allOf(enforce.isArray(), enforce.isArray().longerThan(2));
```
## enforce.oneOf()
enforce.oneOf can be used to determine if _exactly_ one of the rules applies. It will run against rule in the array, and will only pass if exactly one rule applies.
```js
enforce(value).oneOf(
enforce.isString(),
enforce.isNumber(),
enforce.isString().longerThan(1),
);
/*
value = 1 -> ✅ (value is a number)
value = "1" -> ✅ (value is string)
value = [1, 2] -> 🚨 (value is not a string or a number)
value = "12" -> 🚨 (value is both a string and longer than 1)
*/
```
## enforce.noneOf - None rules
enforce.noneOf can be used to determine if _none_ of the rules apply. It will run against each rules supplied, and will only pass if none of the rules pass.
```js
enforce(value).noneOf(
enforce.isString(),
enforce.isNumber(),
enforce.isString().longerThan(1)
);
value = 1 -> 🚨 (value is a number)
value = "1" -> 🚨 (value is string)
value = [1, 2] -> ✅ (value is not a string and not longer than 1)
value = "12" -> 🚨 (value is a string and longer than 1)
```
# Data Parsers
Data parsers transform values as they pass through an enforce chain. Unlike validation rules that only check whether a value is valid, parsers **coerce the value** into a new form — trimming whitespace, converting types, clamping numbers, and more.
Parsers are available on **lazy chains** started with a type rule such as `enforce.isString()`, `enforce.isNumber()`, or `enforce.isArray()`. They are not available on eager `enforce(value)` chains.
## Accessing parsed results
Use `.run()` or `.parse()` to get the transformed output:
```js
// .run() returns { pass, type } — type holds the transformed value
const result = enforce.isString().trim().toUpper().run(' hello ');
// result → { pass: true, type: 'HELLO' }
// .parse() returns the transformed value directly, or throws on failure
const value = enforce.isString().trim().toUpper().parse(' hello ');
// value → 'HELLO'
```
Parsers also work inside schema definitions, so `schema.parse(data)` returns a fully transformed object:
```js
const schema = enforce.shape({
name: enforce.isString().trim().toTitle(),
age: enforce.isNumeric().toNumber().clamp(0, 120),
});
schema.parse({ name: ' jANE DOE ', age: '180' });
// → { name: 'Jane Doe', age: 120 }
```
---
## String Parsers
Available on `enforce.isString()` chains.
### trim
Removes leading and trailing whitespace.
```js
enforce.isString().trim().parse(' vest ');
// → 'vest'
```
### trimStart
Removes leading whitespace only.
```js
enforce.isString().trimStart().parse(' vest');
// → 'vest'
```
### trimEnd
Removes trailing whitespace only.
```js
enforce.isString().trimEnd().parse('vest ');
// → 'vest'
```
### toUpper
Converts to uppercase.
```js
enforce.isString().toUpper().parse('vest');
// → 'VEST'
```
### toLower
Converts to lowercase.
```js
enforce.isString().toLower().parse('VeSt');
// → 'vest'
```
### toTitle
Capitalizes the first letter of each word.
```js
enforce.isString().toTitle().parse('hello world');
// → 'Hello World'
```
### toCapitalized
Capitalizes the first character and lowercases the rest.
```js
enforce.isString().toCapitalized().parse('vEST');
// → 'Vest'
```
### toCamel
Converts to camelCase.
```js
enforce.isString().toCamel().parse('hello_world-test');
// → 'helloWorldTest'
```
### toPascal
Converts to PascalCase.
```js
enforce.isString().toPascal().parse('hello_world-test');
// → 'HelloWorldTest'
```
### toSnake
Converts to snake_case.
```js
enforce.isString().toSnake().parse('helloWorld Test');
// → 'hello_world_test'
```
### toKebab
Converts to kebab-case.
```js
enforce.isString().toKebab().parse('helloWorld Test');
// → 'hello-world-test'
```
### append
Appends a suffix string.
```js
enforce.isString().append('-js').parse('vest');
// → 'vest-js'
```
### prepend
Prepends a prefix string.
```js
enforce.isString().prepend('hello-').parse('vest');
// → 'hello-vest'
```
### replace
Replaces the first match of a search value.
```js
enforce.isString().replace('rocks', 'rules').parse('vest rocks');
// → 'vest rules'
```
### replaceAll
Replaces all occurrences of a search value.
```js
enforce.isString().replaceAll('vest', 'n4s').parse('vest vest vest');
// → 'n4s n4s n4s'
```
### split
Splits a string into an array by a separator. Accepts an optional limit.
```js
enforce.isString().split(',', 2).parse('a,b,c');
// → ['a', 'b']
```
### normalizeWhitespace
Collapses multiple whitespace characters into a single space and trims.
```js
enforce.isString().normalizeWhitespace().parse(' v e s t ');
// → 'v e s t'
```
### stripWhitespace
Removes all whitespace characters.
```js
enforce.isString().stripWhitespace().parse(' v e s t ');
// → 'vest'
```
### removeNonAlphanumeric
Removes all characters that are not letters or digits.
```js
enforce.isString().removeNonAlphanumeric().parse('v-e_s t!42');
// → 'vest42'
```
### removeNonDigits
Removes all non-digit characters.
```js
enforce.isString().removeNonDigits().parse('v1e2s3t');
// → '123'
```
### removeNonLetters
Removes all non-letter characters.
```js
enforce.isString().removeNonLetters().parse('v1e_2s-3t!');
// → 'vest'
```
---
## Number Parsers
Available on `enforce.isNumber()` and `enforce.isNumeric()` chains.
### round
Rounds to the nearest integer.
```js
enforce.isNumber().round().parse(2.5);
// → 3
```
### ceil
Rounds up to the next integer.
```js
enforce.isNumber().ceil().parse(2.1);
// → 3
```
### floor
Rounds down to the previous integer.
```js
enforce.isNumber().floor().parse(2.9);
// → 2
```
### clamp
Clamps a value between a minimum and maximum.
```js
enforce.isNumeric().toNumber().clamp(0, 100).parse('120');
// → 100
enforce.isNumber().clamp(0, 100).parse(-5);
// → 0
```
### toAbsolute
Returns the absolute value.
```js
enforce.isNumber().toAbsolute().parse(-15);
// → 15
```
### toFloat
Parses a value into a floating-point number. Fails if the result is `NaN`.
```js
enforce.isNumeric().toFloat().parse('10.5');
// → 10.5
```
### toInteger
Parses a value into an integer. Accepts an optional radix (2–36, default 10). Fails if the result is `NaN`.
```js
enforce.isNumeric().toInteger().parse('11.8');
// → 11
enforce.isNumeric().toInteger(2).parse('1011');
// → 11
```
### toDate
Parses a string, number, or Date into a `Date` object. Fails for invalid or non-date-like inputs.
```js
enforce.isNumber().toDate().parse(1704067200000);
// → Date object (2024-01-01T00:00:00.000Z)
```
---
## Array Parsers
Available on `enforce.isArray()` chains.
### uniq
Removes duplicate elements (uses `Set`).
```js
enforce.isArray().uniq().parse(['a', 'a', 'b']);
// → ['a', 'b']
```
### join
Joins array elements into a string with a separator (default `','`).
```js
enforce.isArray().join('-').parse(['a', 'b', 'c']);
// → 'a-b-c'
```
---
## General Parsers
Available on **all** typed chains (`isString`, `isNumber`, `isNumeric`, `isArray`).
### toBoolean
Parses a value into a boolean. Recognizes common truthy/falsy representations. Fails for unrecognized input.
**Truthy values:** `true`, `1`, `'true'`, `'1'`, `'yes'`, `'on'`
**Falsy values:** `false`, `0`, `'false'`, `'0'`, `'no'`, `'off'`
```js
enforce.isString().trim().toBoolean().parse(' yes ');
// → true
enforce.isString().trim().toBoolean().parse('0');
// → false
// Fails for unrecognized values:
enforce.isString().toBoolean().run('unknown');
// → { pass: false, type: false, message: 'Could not parse to boolean' }
```
### parseJSON
Parses a JSON string into a JavaScript value. Fails if the input is not valid JSON.
```js
enforce.isString().parseJSON().parse('{"name":"vest"}');
// → { name: 'vest' }
```
### defaultTo
Provides a fallback value for `null` or `undefined` inputs. This parser runs **before** other rules in the chain, so the fallback is applied before type checks.
```js
const schema = enforce.shape({
label: enforce.isString().defaultTo('N/A'),
});
schema.parse({ label: null });
// → { label: 'N/A' }
schema.parse({ label: 'hello' });
// → { label: 'hello' }
```
---
## Chaining Parsers
Parsers can be chained together to build transformation pipelines. Each parser receives the output of the previous one:
```js
const schema = enforce.shape({
name: enforce.isString().trim().toTitle(),
age: enforce.isNumeric().toNumber().clamp(0, 120),
subscribed: enforce.isString().trim().toBoolean(),
tags: enforce.isArray().uniq().join('|'),
payload: enforce.isString().parseJSON(),
nickname: enforce.isString().trim().defaultTo('N/A'),
});
schema.parse({
name: ' jANE DOE ',
age: '180',
subscribed: ' yes ',
tags: ['vest', 'n4s', 'vest'],
payload: '{"env":"test"}',
nickname: ' ',
});
// → {
// name: 'Jane Doe',
// age: 120,
// subscribed: true,
// tags: 'vest|n4s',
// payload: { env: 'test' },
// nickname: '',
// }
```
### TypeScript type inference
Parser chains correctly distinguish between input and output types. The first rule in the chain determines the **input type** (what the caller passes in), and the last parser determines the **output type** (what the consumer receives).
```typescript
// Input: string | number → Output: number
const ageRule = enforce.isNumeric().toNumber().clamp(0, 120);
// Input: string → Output: boolean
const subscribedRule = enforce.isString().trim().toBoolean();
// Input: string[] → Output: string
const tagsRule = enforce.isArray().uniq().join('|');
```
When used inside a Vest suite schema, this means `suite.run()` accepts the input types while the suite callback and `result.value` use the output types — no type casts needed.
# Date Enforce Rules
The date enforce rules provide functionality to validate and manipulate date values. This documentation covers the `isDate`, `isAfter`, `isBefore`, and `isISO8601` rules, along with their options and configurations.
These rule exposes the [`validator.js`](https://www.npmjs.com/package/validator) date rule, and accepts the same options.
## isDate Rule
The `isDate` rule checks whether a given value is a valid date. It accepts various options to customize the validation behavior.
```javascript
enforce(value).isDate(options);
```
### Options
The `isDate` rule accepts an optional `options` object to customize the validation behavior. The available options are as follows:
| Option | Default Value | Description |
| ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `format` | `'YYYY/MM/DD'` | A string specifying the expected date format. The default format is `'YYYY/MM/DD'`. |
| `strictMode` | `false` | A boolean value indicating whether strict mode should be enabled. In strict mode, only strings matching the format are valid. |
| `delimiters` | `['/', '-']` | An array of allowed date delimiters. By default, both `'/'` and `'-'` are considered valid delimiters. |
#### Valid Date Formats
The `format` option accepts various valid date formats, including:
- `'YYYY/MM/DD'`
- `'YY/MM/DD'`
- `'YYYY-MM-DD'`
- `'YY-MM-DD'`
- `'MM/DD/YYYY'`
- `'MM/DD/YY'`
- `'MM-DD-YYYY'`
- `'MM-DD-YY'`
- `'DD/MM/YYYY'`
- `'DD/MM/YY'`
- `'DD-MM-YYYY'`
- `'DD-MM-YY'`
### Usage Example
```javascript
import { enforce } from 'vest';
import 'vest/date';
const dateString = '2002-07-15';
// Basic usage
enforce(dateString).isDate();
// Usage with options
enforce(dateString).isDate({
format: 'YYYY-MM-DD',
strictMode: true,
delimiters: ['-', '/'],
});
```
## isAfter Rule
The `isAfter` rule checks if a given date string is after a specified date. It accepts an optional `comparisonDate` parameter to compare against.
```javascript
enforce(dateString).isAfter(comparisonDate);
```
### Usage Example
```javascript
import { enforce } from 'vest';
import 'vest/date';
const dateString = '2002-07-15';
const comparisonDate = '2002-07-14';
// Basic usage
enforce(dateString).isAfter(comparisonDate);
```
## isBefore Rule
The `isBefore` rule checks if a given date string is before a specified date. It accepts an optional `comparisonDate` parameter to compare against.
```javascript
enforce(dateString).isBefore(comparisonDate);
```
### Usage Example
```javascript
import { enforce } from 'vest';
import 'vest/date';
const dateString = '2002-07-15';
const comparisonDate = '2002-07-16';
// Basic usage
enforce(dateString).isBefore(comparisonDate);
```
## isISO8601 Rule
The `isISO8601` rule checks if a given string is a valid ISO 8601 date. It supports strict mode and strict separator options.
```javascript
enforce(dateString).isISO8601(options);
```
### Options
The `isISO8601` rule accepts an optional `options` object to customize the validation behavior. The available options are as follows:
| Option | Default Value | Description |
| ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `strict` | `false` | A boolean value indicating whether strict mode should be enabled. In strict mode, only valid ISO 8601 date strings are considered. |
| `strictSeparator` | `false` | A boolean value indicating whether strict separator mode should be enabled. In strict separator mode, the date and time separator must be a 'T'. |
### Usage Example
```javascript
import { enforce } from 'vest';
import 'vest/date';
const dateString = '2020-07-10T15:00:00.000';
// Basic usage
enforce(dateString).isISO8601();
// Usage with options
enforce(dateString).isISO8601({
strict: true,
strictSeparator: true,
});
```
# isEmail Enforce Rule
## Description
The `isEmail` enforce rule is used to validate whether a given value is a valid email address. It leverages the `isEmail` library to perform the email validation.
The rule exposes the [`validator.js`](https://www.npmjs.com/package/validator) isEmail rule, and accepts the same options.
## Usage Example
```javascript
import { enforce } from 'vest';
import 'vest/email';
const email = 'user@example.com';
// Basic usage
enforce(email).isEmail();
```
## Options
The `isEmail` enforce rule accepts an optional `options` object to customize the validation behavior. The available options are as follows:
| Option | Default Value | Description |
| ---------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allow_display_name` | `false` | If set to `true`, the validator will also match the format `Display Name `. |
| `require_display_name` | `false` | If set to `true`, the validator will reject strings without the format `Display Name `. |
| `allow_utf8_local_part` | `true` | If set to `false`, the validator will not allow any non-English UTF8 character in the email address's local part. |
| `require_tld` | `true` | If set to `false`, email addresses without a top-level domain (TLD) in their domain will also be matched. |
| `ignore_max_length` | `false` | If set to `true`, the validator will not check for the standard maximum length of an email. |
| `allow_ip_domain` | `false` | If set to `true`, the validator will allow IP addresses in the host part of the email address. |
| `domain_specific_validation` | `false` | If set to `true`, additional validation will be enabled, disallowing certain syntactically valid email addresses that are rejected by Gmail and other services. |
| `blacklisted_chars` | `''` | If a string is provided, the validator will reject emails that include any of the characters in the string in the name part. |
| `host_blacklist` | `[]` | If set to an array of strings, and the part of the email after the `@` symbol matches one of the strings defined in the array, the validation fails. |
| `host_whitelist` | `[]` | If set to an array of strings, and the part of the email after the `@` symbol matches none of the strings defined in the array, the validation fails. |
```js
// Usage with options
enforce(email).isEmail({
allow_display_name: true,
require_display_name: true,
allow_utf8_local_part: false,
require_tld: false,
ignore_max_length: true,
allow_ip_domain: true,
domain_specific_validation: true,
blacklisted_chars: '!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~',
host_blacklist: ['example.com', 'example.org'],
host_whitelist: ['gmail.com', 'yahoo.com'],
});
```
# isURL Enforce Rule
## Description
The isURL enforce rule provides functionality to validate URL values. This documentation covers the `isUrl` rule, along with its options and configurations.
These rule exposes the [`validator.js`](https://www.npmjs.com/package/validator) isURL rule, and accepts the same options.
## isURL Rule
The `isURL` rule checks whether a given value is a valid URL. It accepts various options to customize the validation behavior.
To be used, the rule first needs to be imported:
```javascript
import 'vest/isURL';
enforce(value).isURL(options);
```
### Options
The isURL rule accepts an optional options object to customize the validation behavior. The available options are as follows:
The `isUrl` rule accepts an optional `options` object to customize the validation behavior. The available options are as follows:
| Option | Default Value | Description |
| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------- |
| `require_protocol` | `false` | Requires the URL to include a protocol (e.g., `http://` or `https://`). |
| `require_host` | `true` | Requires the URL to include a host (e.g., `www.example.com`). |
| `require_valid_protocol` | `true` | Requires the URL's protocol to be in the list of valid protocols (`http`, `https`, `ftp`). |
| `allow_underscores` | `false` | Allows underscores in the host name. |
| `allow_trailing_dot` | `false` | Allows a trailing dot in the host name. |
| `allow_protocol_relative_urls` | `false` | Allows protocol-relative URLs (e.g., `//www.example.com`). |
| `allow_fragments` | `true` | Allows URL fragments (e.g., `#section`). |
| `allow_query_components` | `true` | Allows query components in the URL (e.g., `?query=value`). |
| `validate_length` | `true` | Validates that the URL length does not exceed the maximum allowed length (2083 characters). |
### Usage Example
```javascript
// Usage with options
enforce(url).isURL({
protocols: ['http', 'https', 'ftp'],
require_tld: true,
require_protocol: false,
require_host: true,
require_port: false,
require_valid_protocol: true,
allow_underscores: false,
allow_trailing_dot: false,
allow_protocol_relative_urls: false,
allow_fragments: true,
allow_query_components: true,
validate_length: true,
});
```
# Builtin enforce plugins
In order to save up on bundle size, enforce ships with a minimal set of rules. These rules are the most common ones, and are used in most projects. Some rules, such as isEmail, or other schema rules may be useful but less common. These are supported as plugins and can be consumed directly from the `vest/enforce` directory.
The following documents in this section describe the builtin plugins that are included by default and are ready to use.
To consume any of these plugins, simply import them in your project:
```js
import 'vest/email';
```
# Schema rules
While less common when using Vest, sometimes it might be useful to validate a value against a schema. Vest comes with some schema validation rules that are handy for data-shape validation.
To use it, simply use them in your project.
These rules are available in `enforce`:
- [enforce.shape() - Lean schema validation.](#enforceshape---lean-schema-validation)
- [enforce.optional() - nullable values](#enforceoptional---nullable-values)
- [enforce.partial() - allows supplying a subset of keys](#enforcepartial---allows-supplying-a-subset-of-keys)
- [enforce.loose() - loose shape matching](#enforceloose---loose-shape-matching)
- [enforce.pick() - pick a subset of fields](#enforcepick---pick-a-subset-of-fields)
- [enforce.omit() - omit a subset of fields](#enforceomit---omit-a-subset-of-fields)
- [enforce.isArrayOf() / enforce.list() - array shape matching](#enforceisarrayof--enforcelist---array-shape-matching)
- [enforce.record() - dynamic object matching](#enforcerecord---dynamic-object-matching)
- [enforce.lazy() - recursive schemas](#enforcelazy---recursive-schemas)
- [enforce.tuple() - fixed-length array validation](#enforcetuple---fixed-length-array-validation)
## enforce.shape() - Lean schema validation.
`enforce.shape()` validates the structure of an object.
```js
enforce({
firstName: 'Rick',
lastName: 'Sanchez',
age: 70,
}).shape({
firstName: enforce.isString(),
lastName: enforce.isString(),
age: enforce.isNumber(),
});
```
You may also chain your validation rules:
```js
enforce({
age: 22,
}).shape({
age: enforce.isNumber().isBetween(0, 150),
});
```
You may also nest calls to shape in order to validate a deeply nested object.
```js
enforce({
user: {
name: {
first: 'Joseph',
last: 'Weil',
},
},
}).shape({
user: enforce.shape({
name: enforce.shape({
first: enforce.isString(),
last: enforce.isString(),
}),
}),
});
```
### enforce.optional() - nullable values
In regular cases, a missing value would cause a validation failure. To prevent that from happening, mark your optional keys with `enforce.optional`.
enforce.optional will pass validations of a key that's either not defined, undefined or null.
`enforce.optional` takes as its arguments all the rules that their value must pass.
```js
enforce({
firstName: 'Rick',
lastName: 'Sanchez',
}).shape({
firstName: enforce.isString(),
middleName: enforce.optional(enforce.isString()),
lastName: enforce.isString(),
});
```
### enforce.partial() - allows supplying a subset of keys
When supplying a "shape" or a "loose" matcher, enforce requires at least the keys that are specified by the matcher, unless you manually wrap them with "optional". `enforce.partial` is a shorthand for applyong the `optional` modifier on all shape object keys. By wrapping the input of a matcher with `enforce.partial`, you can supply a subset of the keys that are required as if you had used `optional` on each key.
```js
enforce({}).partial({
firstName: enforce.isString(),
lastName: enforce.isString(),
});
```
This won't throw because all the fields are now treated as optional.
### enforce.loose() - loose shape matching
By default, shape will treat excess keys in your data object as validation errors. If you wish to allow support for excess keys in your object's shape, you can use `enforce.loose()` which is a shorthand to `enforce.shape(data, shape, { loose: true })`.
```js
enforce({ name: 'Laura', code: 'x23' }).shape({ name: enforce.isString() });
// 🚨 This will throw an error because `code` is not defined in the shape
```
```js
enforce({ name: 'Laura', code: 'x23' }).loose({ name: enforce.isString() });
// ✅ This will pass with `code` not being validated
```
### enforce.pick() - pick a subset of fields
When you want to validate only a specific subset of fields from an existing schema, you can use `enforce.pick`. This rule validates only the designated fields, ignoring any extra keys present.
```js
enforce({ name: 'Laura', code: 'x23', internal: true }).pick(
{
name: enforce.isString(),
code: enforce.isString(),
internal: enforce.isBoolean(),
},
['name', 'code'],
);
// ✅ This will pass, picking only the `name` and `code` fields for validation
```
### enforce.omit() - omit a subset of fields
When you want to validate an object against a schema but explicitly exclude certain fields from that validation, use `enforce.omit`. The second argument accepts a single key or an array of keys to omit.
```js
enforce({ name: 'Laura', code: 'x23' }).omit(
{
name: enforce.isString(),
code: enforce.isNumber(),
},
'code',
);
// ✅ This will pass, validating `name` but skipping `code`
enforce({ name: 'Laura', code: 'x23', internal: true }).omit(
{
name: enforce.isString(),
code: enforce.isNumber(),
internal: enforce.isBoolean(),
},
['code', 'internal'],
);
// ✅ This will pass, validating only `name` and skipping `code` and `internal`
```
## enforce.isArrayOf() / enforce.list() - array shape matching
`enforce.list()` is an alias for `enforce.isArrayOf()` — they are identical. Use whichever reads better alongside your other schema rules (`shape`, `tuple`, `record`, `loose`, etc.).
enforce.isArrayOf (or enforce.list) can be used to determine the allowed types and values within an array. It will run against each element in the array, and will only pass if all items meet at least one of the validation rules.
```js
enforce([1, 2, 'hello!']).isArrayOf(enforce.isString(), enforce.isNumber());
```
You can also combine `isArrayOf` with other rules to validate other array properties:
```js
enforce(someArrayValue)
.isArrayOf(enforce.isString(), enforce.isNumber().lessThan(3))
.longerThan(2);
```
And as part of shape:
```js
enforce({ data: [1, 2, 3] }).shape({
data: enforce.isArrayOf(enforce.isNumber()),
});
```
## enforce.record() - dynamic object matching
When you need to validate objects acting as key-value mappings (where keys are dynamic), you can use `enforce.record()`. It allows you to validate all values, and optionally all keys, within an object.
```js
// Validating just values
const userRoles = { alice: 'admin', bob: 'editor' };
enforce(userRoles).record(enforce.isString());
```
```js
// Validating both keys and values
// The first argument is the key rule, the second is the value rule
const exactMapping = { user_123: true, user_456: false };
enforce(exactMapping).record(
enforce.isString().matches(/^user_\d+$/),
enforce.isBoolean(),
);
```
Just like `isArrayOf`, `record` can be nested in shapes or other arrays, and properly populates accurate error paths (e.g. `settings.darkMode`).
## enforce.lazy() - recursive schemas
Use `enforce.lazy()` to validate self-referencing data structures like trees, nested comments, or recursive menus. It wraps a factory function that returns the schema, deferring resolution to validation time to avoid infinite recursion during definition.
```js
const treeSchema = enforce.shape({
value: enforce.isNumber(),
children: enforce.isArrayOf(enforce.lazy(() => treeSchema)),
});
treeSchema.test({
value: 1,
children: [
{ value: 2, children: [] },
{
value: 3,
children: [{ value: 4, children: [] }],
},
],
}); // true
```
`lazy` works with `enforce.optional()` for optional recursive fields, such as binary trees:
```js
const binaryTree = enforce.shape({
value: enforce.isNumber(),
left: enforce.optional(enforce.lazy(() => binaryTree)),
right: enforce.optional(enforce.lazy(() => binaryTree)),
});
binaryTree.test({ value: 1 }); // true (leaf node)
binaryTree.test({
value: 1,
left: { value: 2, right: { value: 3 } },
}); // true
```
The factory function is called once on first validation and cached, avoiding repeated schema resolution overhead.
### TypeScript
TypeScript cannot infer recursive types automatically. When using `enforce.lazy()` for recursive schemas, provide an explicit type annotation:
```ts
import type { RuleInstance } from 'n4s';
type Category = {
name: string;
children: Category[];
};
const categorySchema: RuleInstance = enforce.shape({
name: enforce.isString(),
children: enforce.isArrayOf(enforce.lazy(() => categorySchema)),
});
```
For non-recursive uses, the type is inferred automatically:
```ts
const schema = enforce.shape({
name: enforce.isString(),
metadata: enforce.lazy(() =>
enforce.shape({
key: enforce.isString(),
value: enforce.isNumber(),
}),
),
});
type Data = typeof schema.infer;
// { name: string; metadata: { key: string; value: number } }
```
## enforce.tuple() - fixed-length array validation
Use `enforce.tuple()` to validate arrays where each position has a distinct type. Unlike `isArrayOf` which applies the same rule to all elements, `tuple` maps each rule to its corresponding index and enforces exact length.
```js
enforce.tuple(enforce.isString(), enforce.isNumber());
// ✓ ['hello', 42]
// ✗ ['hello', 'world'] — wrong type at index 1
// ✗ ['hello'] — too few elements
// ✗ ['hello', 42, true] — too many elements
```
Only trailing elements can be made optional (elements in the middle must remain required):
```js
enforce.tuple(enforce.isString(), enforce.optional(enforce.isNumber()));
// ✓ ['hello']
// ✓ ['hello', 42]
```
Tuple elements can be any schema rule, including nested shapes and other tuples:
```js
enforce.tuple(
enforce.isString(),
enforce.shape({ lat: enforce.isNumber(), lng: enforce.isNumber() }),
);
// ✓ ['location', { lat: 40.7, lng: -74.0 }]
```
### TypeScript
Tuple types are inferred automatically from the rules:
```ts
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
type T = enforce.infer; // [string, number]
```
Tuples compose naturally inside shapes:
```ts
const pointSchema = enforce.shape({
label: enforce.isString(),
coords: enforce.tuple(enforce.isNumber(), enforce.isNumber()),
});
type Point = enforce.infer;
// { label: string; coords: [number, number] }
```
## Schema Parsing
Schema rules can also **transform** values using built-in data parsers. Parsers like `trim()`, `toNumber()`, and `toBoolean()` coerce data as it passes through the chain, and `schema.parse()` returns the fully transformed result.
```js
const schema = enforce.shape({
name: enforce.isString().trim().toTitle(),
age: enforce.isNumeric().toNumber().clamp(0, 120),
});
schema.parse({ name: ' jANE DOE ', age: '180' });
// → { name: 'Jane Doe', age: 120 }
```
See the full list of available parsers in [Data Parsers](https://vestjs.dev/docs/enforce/builtin-enforce-plugins/data_parsers).
# Composing Enforce Rules
You can combine multiple enforce rules into a single reusable validator using `compose`.
## Why Compose?
Sometimes you have a set of rules that always go together. For example, a "valid age" might always need to be:
1. A number
2. At least 18
3. Less than 120
Instead of repeating these three checks every time, you can compose them into a single `isValidAge` rule. This promotes reuse and keeps your schemas clean.
```javascript
import { enforce, compose } from 'vest';
const isValidAge = compose(
enforce.isNumber(),
enforce.greaterThanOrEquals(18),
enforce.lessThan(120),
);
// Usage
isValidAge.run(20); // { pass: true }
isValidAge.run(15); // { pass: false }
// Inside a schema
const userSchema = enforce.shape({
age: isValidAge,
});
```
Composed rules act just like standard enforce rules and can be used for type guards, schema validation, or standalone assertions.
# Consuming External Rules with Enforce
Enforce is a versatile assertion library that provides a wide range of validation rules to ensure the validity of input data in your app. However, in some cases, you may need additional validation rules that are not included in Enforce's core library. This is where external rules come in.
## The Need for External Rules
Enforce includes the most common rules needed for input validation and does not make assumptions about your business logic constraints. This is why it does not include certain validation rules such as `isCurrency`, which may be required for your app's validation needs.
Fortunately, there are numerous packages available, such as `validator.js`, that provide additional validation rules. `validator.js` is a popular and highly compatible package that can be used in conjunction with Enforce to add these rules to your app's validation.
## Importing External Rules
Before you can use external rules with Enforce, you need to install and import the relevant packages. For example, to use the `isCurrency` and `isMobilePhone` rules from `validator.js`, you would install the package using npm:
```
npm i validator
```
Then, in your code, you can import the individual rules that you need:
```js
import isCurrency from 'validator/es/lib/isCurrency';
import isMobilePhone from 'validator/es/lib/isMobilePhone';
```
Note that importing the entire `validator.js` package can increase your bundle size unnecessarily, so it is recommended to import only the individual rules that you need.
## Adding External Rules to Enforce
Once you have imported the relevant external rules, you can add them to Enforce's library of validation rules using the `enforce.extend` method:
```js
enforce.extend({ isCurrency, isMobilePhone });
```
This method takes an object that maps the rule name to the validation function. In this example, `isCurrency` and `isMobilePhone` are mapped to their respective validation functions.
## Using External Rules with Enforce
After adding the external rules to Enforce, you can use them in your validation tests just like any other Enforce rule. Here's an example that uses the `isCurrency` rule to validate a currency address:
```js
enforce('$').isCurrency(); // ✅
enforce('...').isCurrency(); // 🚨
```
In this example, the `enforce` function is called with a string value as input data. The `isCurrency` rule is then called to validate the currency.
A full list of the supported validator.js rules can be found on [npmjs.com/package/validator](https://www.npmjs.com/package/validator).
# Creating Custom Rules
By default, enforce comes with a list of rules that are available to be used. They intentionally do not cover all the cases that can be encountered in a real-world application but instead focus on the most common use cases.
## Why Custom Rules?
Every application has unique domain logic. You might need to validate:
- A specific product SKU format.
- That a start date is before an end date.
- That a username exists in your database.
Custom rules allow you to extend Vest's vocabulary to speak your domain language.
Use `condition` for one-off logic. Use `enforce.extend` when a rule represents stable vocabulary that should be shared across suites or schemas.
## Inline logic with `condition`
Sometimes you would need to add some custom logic to your validation. For that you can use `enforce.condition` which accepts a function.
Your provided function will receive the enforced value and returns either a boolean or a rule-return object.
```js
// Passes if the value is `1`
enforce(1).condition(value => {
return value === 1;
});
```
```js
enforce(2).condition(value => {
return {
pass: value === 1,
message: 'value must be one',
};
});
```
## Reusable custom rules with enforce.extend
To make it easier to reuse logic across your application, sometimes you would want to encapsulate bits of logic in rules that you can use later on, for example, "what's considered a valid email".
import CustomRulesSandpack from '@site/src/components/Sandpack/CustomRules';
Rules are called with the argument passed to enforce(x) followed by the arguments passed to `.yourRule(y, z)`.
```js
enforce.extend({
isValidEmail: value => value.indexOf('@') > -1,
hasKey: (value, key) => value.hasOwnProperty(key),
passwordsMatch: (passConfirm, options) =>
passConfirm === options.passConfirm && options.passIsValid,
});
enforce(user.email).isValidEmail();
```
## Custom rules return value
Rules can return a boolean or a rule-result object. `pass` indicates success, and `message` may be a string or a function that returns the failure message.
```js
enforce.extend({
isWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () =>
`expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});
```
## Context Aware Rules
Custom rules can access the validation context using `enforce.context()`. This is useful when validating schemas where a rule needs to know about other fields (e.g., "confirm password").
```javascript
import { enforce } from 'vest';
enforce.extend({
matchesField: (value, fieldName) => {
const context = enforce.context();
const parent = context?.parent()?.value;
return value === parent?.[fieldName];
},
});
export const schema = enforce.shape({
confirm: enforce.isString().matchesField('password'),
password: enforce.isString(),
});
```
`parent` is a function in Vest 6 and context may be absent, so context-aware rules should use nullable access. Put a sibling-dependent rule before the sibling it reads so the parent context still contains the complete source object. Nested arrays or objects may require more than one `parent()` traversal.
## TypeScript Support
To ensure your custom rules are typed correctly in your IDE, you must extend the `n4s` namespace.
```typescript
// customRules.ts
import { enforce } from 'vest';
const customRules = {
isValidEmail: (value: string) => value.includes('@'),
isWithinRange: (value: number, min: number, max: number) =>
value >= min && value <= max,
};
enforce.extend(customRules);
// Extend the interface to add types
declare global {
namespace n4s {
interface EnforceMatchers {
isValidEmail: (value: string) => boolean;
isWithinRange: (value: number, min: number, max: number) => boolean;
}
}
}
```
_Note: In the interface definition, include the `value` as the first argument._
# Enforce: The Assertion Library for Vest
Enforce is the assertion library used inside Vest tests. Its rules are:
- **Fluent** - Chain multiple assertions together naturally
- **Composable** - Build reusable validators from smaller pieces
- **Extensible** - Add your own custom rules
## Basic Usage
Import `enforce` from Vest and use it inside your tests:
import EnforcePlayground from '@site/src/components/Sandpack/EnforcePlayground';
```js
import { enforce, test } from 'vest';
test('username', 'Must be at least three characters long', () => {
enforce(username).longerThan(2);
});
```
When an assertion fails, it throws an error that Vest catches and records as a failed test.
## Fluent Chaining
Chain multiple assertions together to test various conditions:
All assertions must pass for the test to pass. If any assertion fails, the test stops at that point.
## Common Patterns
### Validating Strings
```js
// Required field
enforce(email).isNotBlank();
// Email format
enforce(email).isEmail();
// Length constraints
enforce(password).longerThanOrEquals(8).shorterThanOrEquals(128);
```
### Validating Numbers
```js
// Range check
enforce(age).isNumber().greaterThanOrEquals(18).lessThan(120);
// Positive number
enforce(price).isPositive();
```
### Validating Objects
```js
// Check shape/structure
enforce(user).shape({
name: enforce.isString(),
email: enforce.isEmail(),
age: enforce.isNumber(),
});
```
## Composing Rules
For rules you use together frequently, create reusable validators:
```js
import { enforce, compose } from 'vest';
const isValidAge = compose(
enforce.isNumber(),
enforce.greaterThanOrEquals(18),
enforce.lessThan(120),
);
// Use like any other rule
test('age', 'Must be a valid age', () => {
enforce(data.age).condition(isValidAge);
});
```
:::tip Functional Programming
Enforce rules are **just functions**. The `compose` utility lets you build complex validators from simple, testable pieces - exactly like function composition in FP.
:::
[Learn more about composing rules →](https://vestjs.dev/docs/enforce/composing_enforce_rules)
## Available Rules
Enforce comes with a rich set of built-in rules:
| Category | Examples |
| ---------------- | -------------------------------------------------------------- |
| **Type Checks** | `isString()`, `isNumber()`, `isBoolean()`, `isArray()` |
| **String Rules** | `isNotBlank()`, `isEmail()`, `matches()`, `startsWith()` |
| **Number Rules** | `greaterThan()`, `lessThan()`, `isPositive()`, `isNegative()` |
| **Comparison** | `equals()`, `notEquals()`, `inside()`, `notInside()` |
| **Collection** | `lengthEquals()`, `longerThan()`, `shorterThan()`, `isEmpty()` |
| **Shape** | `shape()`, `loose()`, `isArrayOf()` |
[View all rules →](https://vestjs.dev/docs/enforce/enforce_rules)
## Custom Rules
Need validation logic that isn't built-in? Create your own:
```js
import { enforce } from 'vest';
enforce.extend({
isValidUsername(value) {
return /^[a-zA-Z0-9_]+$/.test(value);
},
});
// Now use it anywhere
enforce(username).isValidUsername();
```
[Learn more about custom rules →](https://vestjs.dev/docs/enforce/creating_custom_rules)
## Next Steps
- [All Built-in Rules](https://vestjs.dev/docs/enforce/enforce_rules) - Complete reference
- [Composing Rules](https://vestjs.dev/docs/enforce/composing_enforce_rules) - Build reusable validators
- [Custom Rules](https://vestjs.dev/docs/enforce/creating_custom_rules) - Extend with your own logic
# List of Enforce rules
Enforce rules are functions that allow you to test your data against different criteria. The following rules are supported out-of-the-box.
- [equals](#equals)
- [notEquals](#notequals)
- [isEmpty](#isempty)
- [isNotEmpty](#isnotempty)
- [isNumeric](#isnumeric)
- [isNotNumeric](#isnotnumeric)
- [greaterThan](#greaterthan)
- [greaterThanOrEquals](#greaterthanorequals)
- [lengthEquals](#lengthequals)
- [lengthNotEquals](#lengthnotequals)
- [lessThan](#lessthan)
- [lessThanOrEquals](#lessthanorequals)
- [longerThan](#longerthan)
- [longerThanOrEquals](#longerthanorequals)
- [numberEquals](#numberequals)
- [numberNotEquals](#numbernotequals)
- [shorterThan](#shorterthan)
- [shorterThanOrEquals](#shorterthanorequals)
- [matches](#matches)
- [notMatches](#notmatches)
- [inside](#inside)
- [notInside](#notinside)
- [isTruthy](#istruthy)
- [isFalsy](#isfalsy)
- [isArray](#isarray)
- [isNotArray](#isnotarray)
- [isBoolean](#isboolean)
- [isNotBoolean](#isnotboolean)
- [isBlank](#isblank)
- [isNotBlank](#isnotblank)
- [isNumber](#isnumber)
- [isNotNumber](#isnotnumber)
- [isNaN](#isnan)
- [isNotNaN](#isnotnan)
- [isNull](#isnull)
- [isNotNull](#isnotnull)
- [isNullish](#isnullish)
- [isNotNullish](#isnotnullish)
- [isString](#isstring)
- [isNotString](#isnotstring)
- [isUndefined](#isundefined)
- [isOdd](#isodd)
- [isEven](#iseven)
- [isBetween](#isbetween)
- [isNotBetween](#isnotbetween)
- [endsWith](#endswith)
- [doesNotEndWith](#doesnotendwith)
- [startsWith](#startswith)
- [doesNotStartWith](#doesnotstartwith)
- [isNegative](#isnegative)
- [isPositive](#ispositive)
- [isValueOf](#isvalueof)
- [isNotValueOf](#isnotvalueof)
- [isKeyOf](#iskeyof)
- [isNotKeyOf](#isnotkeyof)
- [isEmail](https://vestjs.dev/docs/enforce/builtin-enforce-plugins/email)
- [isDate](https://vestjs.dev/docs/enforce/builtin-enforce-plugins/date)
- [isAfter](https://vestjs.dev/docs/enforce/builtin-enforce-plugins/date)
- [isBefore](https://vestjs.dev/docs/enforce/builtin-enforce-plugins/date)
- [isISO8601](https://vestjs.dev/docs/enforce/builtin-enforce-plugins/date)
## equals
### Description
Checks if your enforced value strictly equals (`===`) another.
It is not recommended to use this rule to compare arrays or objects, as it does not perform any sort of deep comparison on the value.
For numeric value comparison, you should use `numberEquals`, which coerces numeric strings into numbers before comparing.
### Arguments
- `value`: Any value you wish to check your enforced value against
### Usage examples:
```js
enforce(1).equals(1);
enforce('hello').equals('hello');
const a = [1, 2, 3];
enforce(a).equals(a);
// passes
```
```js
enforce('1').equals(1);
enforce([1, 2, 3]).equals([1, 2, 3]);
// throws
```
## notEquals
### Description
Checks if your enforced value does not strictly equal (`===`) another.
Reverse implementation of `equals`.
### Usage examples:
```js
enforce('1').notEquals(1);
enforce([1, 2, 3]).notEquals([1, 2, 3]);
// passes
```
```js
enforce(1).notEquals(1);
enforce('hello').notEquals('hello');
const a = [1, 2, 3];
enforce(a).notEquals(a);
// throws
```
## isEmpty
### Description
Checks if your enforced value is empty, false, zero, null or undefined.
Expected results are:
- object: checks against the count of keys (`0` is empty)
- array/string: checks against length. (`0` is empty)
- number: checks the value of the number. (`0` and `NaN` are empty)
- boolean: `false` is empty.
- undefined/null: are both empty.
### Usage examples:
```js
enforce([]).isEmpty();
enforce('').isEmpty();
enforce({}).isEmpty();
enforce(0).isEmpty();
enforce(NaN).isEmpty();
enforce(undefined).isEmpty();
enforce(null).isEmpty();
enforce(false).isEmpty();
// passes
```
```js
enforce([1]).isEmpty();
enforce('1').isEmpty();
enforce({ 1: 1 }).isEmpty();
enforce(1).isEmpty();
enforce(true).isEmpty();
// throws
```
## isNotEmpty
### Description
Checks that your enforced value is not empty, false, or zero.
Reverse implementation of `isEmpty`.
### Usage examples:
```js
enforce([1]).isNotEmpty();
enforce('1').isNotEmpty();
enforce({ 1: 1 }).isNotEmpty();
// passes
```
```js
enforce([]).isNotEmpty();
enforce('').isNotEmpty();
enforce({}).isNotEmpty();
enforce(0).isNotEmpty();
// throws
```
## isNumeric
### Description
Checks if a value is a representation of a real number
### Usage examples:
```js
enforce(143).isNumeric();
enforce('143').isNumeric();
// passes
```
```js
enforce(NaN).isNumeric();
enforce('1hello').isNumeric();
enforce('hi').isNumeric();
// throws
```
## isNotNumeric
### Description
Checks if a value is not a representation of a real number.
Reverse implementation of `isNumeric`.
### Usage examples:
```js
enforce(NaN).isNotNumeric();
enforce('Hello World!').isNotNumeric();
// passes
```
```js
enforce(731).isNotNumeric();
enforce('42').isNotNumeric();
// throws
```
## greaterThan
- alias: `gt`
### Description
Checks that your numeric enforced value is larger than a given numeric value.
### Arguments
- `value`: `number | string` | A numeric value against which you want to check your enforced value.
Strings are parsed using `Number()`, values which are not fully numeric always return false;
### Usage
```js
enforce(1).greaterThan(0);
enforce('10').greaterThan(0);
enforce(900).gt('100');
// passes
```
```js
enforce(100).greaterThan(100);
enforce('100').greaterThan(110);
enforce([100]).gt(1);
// throws
```
## greaterThanOrEquals
- alias: `gte()`
### Description
Checks that your numeric enforced value is larger than or equals a given numeric value.
### Arguments
- `value`: `number | string` | A numeric value against which you want to check your enforced value.
Strings are parsed using `Number()`, values which are not fully numeric always return false;
### Usage
```js
enforce(1).greaterThanOrEquals(0);
enforce('10').greaterThanOrEquals(0);
enforce(900).greaterThanOrEquals('100');
enforce(100).greaterThanOrEquals('100');
enforce(900).gte('900');
enforce('1337').gte(1337);
// passes
```
```js
enforce(100).greaterThanOrEquals('120');
enforce('100').greaterThanOrEquals(110);
enforce([100]).gte(1);
// throws
```
## lengthEquals
### Description
Checks that your enforced value is equal to the given number.
### Arguments
- `size`: `number` | the number which you would like your initial value to be tested against.
The `value` argument can be of the following types:
- array: checks against length.
- string: checks against length.
### Usage examples:
```js
enforce([1]).isArray().lengthEquals(1);
enforce('a').isString().lengthEquals(1);
// passes
```
```js
enforce([1, 2]).isArray().lengthEquals(1);
enforce('').isString().lengthEquals(1);
// throws
```
## lengthNotEquals
### Description
Checks that your enforced value is not equal to the given number.
Reverse implementation of `lengthEquals`.
### Arguments
- `size`: `number` | the number which you would like your initial value to be tested against.
The `value` argument can be of the following types:
- array: checks against length.
- string: checks against length.
### Usage examples:
```js
enforce([1]).isArray().lengthNotEquals(0);
enforce('a').isString().lengthNotEquals(3);
// passes
```
```js
enforce([1]).isArray().lengthNotEquals(1);
enforce('').isString().lengthNotEquals(0);
// throws
```
## lessThan
- alias: `lt()`
### Description
Checks that your numeric enforced value is smaller than a given numeric value.
### Arguments
- `value`: `number | string` | A numeric value against which you want to check your enforced value.
Strings are parsed using `Number()`, values which are not fully numeric always return false;
### Usage
```js
enforce(0).isNumber().lessThan(1);
enforce(2).isNumber().lessThan('10');
enforce('90').isNumeric().lt(100);
// passes
```
```js
enforce(100).isNumber().lessThan(100);
enforce('110').isNumeric().lessThan(100);
enforce([0]).isArray().lt(1);
// throws
```
## lessThanOrEquals
- alias: `lte()`
### Description
Checks that your numeric enforced value is smaller than or equals a given numeric value.
### Arguments
- `value`: `number | string` | A numeric value against which you want to check your enforced value.
Strings are parsed using `Number()`, values which are not fully numeric always return false;
### Usage
```js
enforce(0).isNumber().lessThanOrEquals(1);
enforce(2).isNumber().lessThanOrEquals('10');
enforce('90').isNumeric().lte(100);
enforce(100).isNumber().lte('100');
// passes
```
```js
enforce(100).isNumber().lessThanOrEquals(90);
enforce('110').isNumeric().lessThanOrEquals(100);
enforce([0]).isArray().lte(1);
// throws
```
## longerThan
### Description
Checks that your enforced value is longer than a given number.
### Arguments
- `size`: `number` | the number which you would like your initial value to be tested against.
The `value` argument can be of the following types:
- array: checks against length.
- string: checks against length.
### Usage examples:
```js
enforce([1]).isArray().longerThan(0);
enforce('ab').isString().longerThan(1);
// passes
```
```js
enforce([1]).isArray().longerThan(2);
enforce('').isString().longerThan(0);
// throws
```
## longerThanOrEquals
### Description
Checks that your enforced value is longer than or equals a given number.
### Arguments
- `size`: `number` | the number which you would like your initial value to be tested against.
The `value` argument can be of the following types:
- array: checks against length.
- string: checks against length.
### Usage examples:
```js
enforce([1]).isArray().longerThanOrEquals(0);
enforce('ab').isString().longerThanOrEquals(1);
enforce([1]).isArray().longerThanOrEquals(1);
enforce('a').isString().longerThanOrEquals(1);
// passes
```
```js
enforce([1]).isArray().longerThanOrEquals(2);
enforce('').isString().longerThanOrEquals(1);
// throws
```
## numberEquals
### Description
Checks that your numeric enforced value is equals another value.
### Arguments
- `value`: `number | string` | A numeric value against which you want to check your enforced value.
Strings are parsed using `Number()`, values which are not fully numeric always return false;
### Usage
```js
enforce(0).isNumber().numberEquals(0);
enforce(2).isNumber().numberEquals('2');
// passes
```
```js
enforce(100).isNumber().numberEquals(10);
enforce('110').isNumeric().numberEquals(100);
enforce([0]).isArray().numberEquals(1);
// throws
```
## numberNotEquals
### Description
Checks that your numeric enforced value does not equal another value.
Reverse implementation of `numberEquals`.
### Arguments
- `value`: `number | string` | A numeric value against which you want to check your enforced value.
Strings are parsed using `Number()`, values which are not fully numeric always return false;
### Usage
```js
enforce(2).isNumber().numberNotEquals(0);
enforce('11').isNumeric().numberNotEquals('10');
// passes
```
```js
enforce(100).isNumber().numberNotEquals(100);
enforce('110').isNumeric().numberNotEquals(100);
// throws
```
## shorterThan
### Description
Checks that your enforced value is shorter than a given number.
### Arguments
- `size`: `number` | the number which you would like your initial value to be tested against.
The `value` argument can be of the following types:
- array: checks against length.
- string: checks against length.
### Usage examples:
```js
enforce([]).isArray().shorterThan(1);
enforce('a').isString().shorterThan(2);
// passes
```
```js
enforce([1]).isArray().shorterThan(0);
enforce('').isString().shorterThan(0);
// throws
```
## shorterThanOrEquals
### Description
Checks that your enforced value is shorter than or equals a given number.
### Arguments
- `size`: `number` | the number which you would like your initial value to be tested against.
The `value` argument can be of the following types:
- array: checks against length.
- string: checks against length.
### Usage examples:
```js
enforce([]).isArray().shorterThanOrEquals(1);
enforce('a').isString().shorterThanOrEquals(2);
enforce([]).isArray().shorterThanOrEquals(0);
enforce('a').isString().shorterThanOrEquals(1);
// passes
```
```js
enforce([1]).isArray().shorterThanOrEquals(0);
enforce('ab').isString().shorterThanOrEquals(1);
// throws
```
## matches
### Description
Checks if a value contains a regex match.
### Arguments
- `regexp`: either a `RegExp` object, or a RegExp valid string
### Usage examples:
```js
enforce(1984).matches(/[0-9]/);
enforce(1984).matches('[0-9]');
enforce('1984').matches(/[0-9]/);
enforce('1984').matches('[0-9]');
enforce('198four').matches(/[0-9]/);
enforce('198four').matches('[0-9]');
// passes
```
```js
enforce('ninety eighty four').matches(/[0-9]/);
enforce('ninety eighty four').matches('[0-9]');
// throws
```
## notMatches
### Description
Checks if a value does not contain a regex match.
Reverse implementation of `matches`.
### Usage examples:
```js
enforce(1984).notMatches(/[0-9]/);
// throws
```
```js
enforce('ninety eighty four').notMatches('[0-9]');
// passes
```
## inside
### Description
Checks if your enforced value is contained in another array or string.
Your enforced value can be of the following types:
- `string`
- `number`
- `boolean`
### Arguments
- `container`: a `string` or an `array` which may contain the value specified.
### Usage examples:
#### inside: array
Checks for membership in an array.
- string: checks if a string is an element in an array
```js
enforce('hello').inside(['hello', 'world']);
// passes
```
```js
enforce('hello!').inside(['hello', 'world']);
// throws
```
- number: checks if a number is an element in an array
```js
enforce(1).inside([1, 2]);
// passes
```
```js
enforce(3).inside([1, 2]);
// throws
```
- boolean: checks if a number is an element in an array
```js
enforce(false).inside([true, false]);
// passes
```
```js
enforce(true).inside([1, 2, 3]);
// throws
```
#### inside: string
- string: checks if a string is inside another string
```js
enforce('da').inside('tru dat.');
// passes
```
```js
enforce('ad').inside('tru dat.');
// throws
```
## notInside
### Description
Checks if a given value is not contained in another array or string.
Reverse implementation of `inside`.
### Usage examples:
```js
enforce('ad').notInside('tru dat.');
enforce('hello!').notInside(['hello', 'world']);
// passes
```
```js
enforce('hello').notInside(['hello', 'world']);
enforce('da').notInside('tru dat.');
// throws
```
## isTruthy
### Description
Checks if a value is truthy; Meaning: if it can be coerced into boolean `true`.
Anything not in the following list is considered to be truthy.
- `undefined`
- `null`
- `false`
- `0`
- `NaN`
- empty string (`""`)
### Usage examples:
```js
enforce('hello').isTruthy();
enforce(true).isTruthy();
enforce(1).isTruthy();
// passes
```
```js
enforce(false).isTruthy();
enforce(null).isTruthy();
enforce(undefined).isTruthy();
enforce(0).isTruthy();
enforce(NaN).isTruthy();
enforce('').isTruthy();
// throws
```
## isFalsy
### Description
Checks if a value is falsy; Meaning: if it can be coerced into boolean `false`.
Reverse implementation of `isTruthy`.
Anything not in the following list is considered to be truthy:
- `undefined`
- `null`
- `false`
- `0`
- `NaN`
- empty string (`""`)
### Usage examples:
```js
enforce(false).isFalsy();
enforce(0).isFalsy();
enforce(undefined).isFalsy();
// passes
```
```js
enforce(1).isFalsy();
enforce(true).isFalsy();
enforce('hi').isFalsy();
// throws
```
## isArray
### Description
Checks if a value is of type `Array`.
### Usage examples:
```js
enforce(['hello']).isArray();
// passes
```
```js
enforce('hello').isArray();
// throws
```
## isNotArray
### Description
Checks if a value is of any type other than `Array`.
Reverse implementation of `isArray`.
### Usage examples:
```js
enforce(['hello']).isNotArray();
// throws
```
```js
enforce('hello').isNotArray();
// passes
```
## isBoolean
### Description
Checks if a value is of type `boolean`.
Equals `typeof value === 'boolean'`
### Usage examples:
```js
enforce(true).isBoolean();
enforce(false).isBoolean();
enforce(!!0).isBoolean();
// passes
```
```js
enforce([]).isBoolean();
enforce('143').isBoolean();
enforce('false').isBoolean();
// throws
```
## isNotBoolean
### Description
Checks if a value is of any type other than `boolean`.
Reverse implementation of `isBoolean`.
### Usage examples:
```js
enforce('143').isNotBoolean();
enforce(143).isNotBoolean();
// passes
```
```js
enforce(true).isNotBoolean();
enforce(false).isNotBoolean();
// throws
```
## isBlank
### Description
Determines whether an enforced string contains only whitespaces. It will also check for null or undefined. All other values will return `false` - to not be confused with `isEmpty`.
### Usage examples:
```js
enforce(' ').isBlank(); // passes
enforce('not blank').isBlank(); // throws
```
## isNotBlank
### Description
Determines whether an enforced string contains at least a non-whitespace character. Will also return true for `null` or `undefined`. Any other value will return `true`. This is the reverse of `isBlank`.
### Usage examples:
```js
enforce('not blank').isNotBlank(); // passes
enforce(' ').isNotBlank(); // throws
```
## isNumber
### Description
Checks if a value is of type `number`.
Equals `typeof value === 'number'`
### Usage examples:
```js
enforce(143).isNumber();
enforce(NaN).isNumber(); // (NaN is of type 'number!')
// passes
```
```js
enforce([]).isNumber();
enforce('143').isNumber();
// throws
```
## isNotNumber
### Description
Checks if a value is of any type other than `number`.
Reverse implementation of `isNumber`.
**note** isNotNumber does not check for `NaN` value. For NaN values, use [isNaN](#isnan).
### Usage examples:
```js
enforce('143').isNotNumber();
enforce(143).isNotNumber();
// passes
```
```js
enforce(143).isNotNumber();
enforce(NaN).isNotNumber(); // throws (NaN is of type 'number!')
// throws
```
## isNaN
### Description
A wrapper around JavaScripts Number.isNaN() function. Checks if a value is NaN.
### Usage examples:
```js
enforce(NaN).isNaN();
enforce('A' / 'B').isNaN();
// passes
```
```js
enforce(null * null).isNaN(); // null*null = 0
enforce(200).isNaN();
enforce('1984').isNaN();
// throws
```
## isNotNaN
### Description
Reverse implementation of `isNaN`. Checks that a value is not NaN.
### Usage examples:
```js
enforce(null * null).isNaN(); // null*null = 0
enforce(200).isNaN();
enforce('1984').isNaN();
// passes
```
```js
enforce(NaN).isNaN();
enforce('A' / 'B').isNaN();
// throws
```
## isNull
### Description
Enforces that a specified value is `null`.
### Usage examples:
```js
enforce(null).isNull();
// passes
```
```js
enforce(undefined).isNull();
enforce(true).isNull();
// throws
```
## isNotNull
### Description
Reverse implementation of `isNull`. Checks that a value is not null.
### Usage examples:
```js
enforce('hello').isNull();
enforce(200).isNull();
// passes
```
```js
enforce(null).isNull();
// throws
```
## isNullish
### Description
Checks if a value is either `null` or `undefined`.
### Usage examples:
```js
enforce(null).isNullish(); // passes
enforce(undefined).isNullish(); // passes
```
```js
enforce('hello').isNullish(); // throws
enforce(200).isNullish(); // throws
```
## isNotNullish
### Description
Checks if a value is not `null` or `undefined`.
### Usage examples:
```js
enforce('hello').isNotNullish(); // passes
enforce(200).isNotNullish(); // passes
```
```js
enforce(null).isNotNullish(); // throws
enforce(undefined).isNotNullish(); // throws
```
## isString
### Description
Checks if a value is of type `String`.
### Usage examples:
```js
enforce('hello').isString();
// passes
```
```js
enforce(['hello']).isString();
enforce(1984).isString();
// throws
```
## isNotString
### Description
Checks if a value is of any type other than `String`.
Reverse implementation of `isString`.
### Usage examples:
```js
enforce('hello').isNotString();
// throws
```
```js
enforce(['hello']).isNotString();
// passes
```
## isUndefined
### Description
Enforces that a given value is (`===`) undefined.
### Usage examples:
```js
enforce().isUndefined();
enforce(undefined).isUndefined();
// passes
```
```js
enforce(null).isUndefined();
enforce(true).isUndefined();
// throws
```
## isOdd
### Description
Checks if a value is an odd numeric value.
### Usage examples:
```js
enforce('1').isOdd();
enforce(9).isOdd();
// passes
```
```js
enforce(2).isOdd();
enforce('4').isOdd();
enforce('1withNumber').isOdd();
enforce([1]).isOdd();
// throws
```
## isEven
### Description
Checks if a value is an even numeric value.
### Usage examples:
```js
enforce(0).isEven();
enforce('2').isEven();
// passes
```
```js
enforce(1).isEven();
enforce('3').isEven();
enforce('2withNumber').isEven();
enforce([0]).isEven();
// throws
```
## isBetween
### Description
Checks if a number is in the range of two numbers (edges of range included)
### Usage examples:
```js
enforce(5).isBetween(0, 5);
enforce(5).isBetween(0, 10);
enforce(-5).isBetween(-10, -1);
enforce(-5.5).isBetween(-10, -1);
enforce(-5.5).isBetween(-10, -1.1);
enforce(-5.5).isBetween(-9.5, -1.1);
// passes
```
```js
enforce(5).isBetween(0, 2);
enforce(-5).isBetween(0, 2);
enforce('some_string').isBetween(0, 2);
enforce(false).isBetween(0, 2);
// throws
```
## isNotBetween
### Description
Checks if a number **is not** in range of two numbers (edges of range excluded)
### Usage examples:
```js
enforce(5).isNotBetween(0, 4);
enforce(5).isNotBetween(0, 10);
enforce(-5).isNotBetween(-10, -1);
enforce(-5.5).isNotBetween(-10, -1);
enforce(-5.5).isNotBetween(-10, -1.1);
// passes
```
```js
enforce(5).isNotBetween(0, 5);
enforce(5).isNotBetween(0, 10);
enforce(-5).isNotBetween(-10, -1);
enforce(-5).isNotBetween(-5, -1);
enforce('some_string').isNotBetween(0, 2);
enforce(false).isNotBetween(0, 2);
// throws
```
## endsWith
### Description
Determines whether a string ends with the characters of a specified string.
### Usage examples:
```js
enforce('aba').endsWith('ba');
enforce('some_string').endsWith('_string');
enforce('string with spaces').endsWith('ng with spaces');
enforce('aaaa ').endsWith(' ');
// passes
```
```js
enforce('for').endsWith('tor');
enforce('aaaab').endsWith('aaaa');
enforce('aa').endsWith('aaa');
enforce(42).endsWith('b');
enforce(42).endsWith(50);
enforce(true).endsWith(100);
// throws
```
## doesNotEndWith
### Description
Determines whether a string does not end with the characters of a specified string.
Reverse implementation of `endsWith`.
## startsWith
### Description
Determines whether a string starts with the characters of a specified string.
### Usage examples:
```js
enforce('aba').startsWith('ab');
enforce('some_string').startsWith('some_');
enforce('string with spaces').startsWith('string with s');
enforce('aaaa ').startsWith('aaaa ');
// passes
```
```js
enforce('for').startsWith('tor');
enforce('aaaab').startsWith('aab');
enforce('aa').startsWith('aaa');
enforce(42).startsWith('b');
enforce(42).startsWith(50);
enforce(true).startsWith(100);
// throws
```
## doesNotStartWith
### Description
Determines whether a string does not start with the characters of a specified string.
Reverse implementation of `startsWith`.
### Usage examples:
```js
enforce('for').doesNotStartWith('tor');
enforce('aaaab').doesNotStartWith('aab');
enforce('aa').doesNotStartWith('aaa');
enforce(42).doesNotStartWith('b');
enforce(42).doesNotStartWith(50);
enforce(true).doesNotStartWith(100);
// passes
```
```js
enforce('aba').doesNotStartWith('ab');
enforce('some_string').doesNotStartWith('some_');
enforce('string with spaces').doesNotStartWith('string with s');
enforce('aaaa ').doesNotStartWith('aaaa ');
// throws
```
## isNegative
### Description
Determines whether a numeric value is negative or not.
### Usage examples:
```js
enforce(-10).isNegative(); //passes
enforce(-10.12).isNegative(); //passes
enforce('-10.12').isNegative(); //passes
enforce(10).isNegative(); // throws
enforce('10').isNegative(); // throws
enforce(0).isNegative(); // throws
```
## isPositive
### Description
Determines whether a numeric value is positive or not.
### Usage examples:
```js
enforce(10).isPositive(); //passes
enforce(10.12).isPositive(); //passes
enforce('10.12').isPositive(); //passes
enforce(-10).isPositive(); // throws
enforce('-10.12').isPositive(); // throws
enforce(0).isPositive(); // throws
```
## isValueOf
### Description
Determines whether a value exists as inside an object.
### Usage examples:
```js
enforce('Bravo').isValueOf({ a: 'Alpha', b: 'Bravo', c: 'Charlie' });
// passes
```
```js
enforce('Delta').isValueOf({ a: 'Alpha', b: 'Bravo', c: 'Charlie' });
// throws
```
## isNotValueOf
### Description
Determines whether a string is not a value of an object..
### Usage examples:
```js
enforce('Delta').isNotValueOf({ a: 'Alpha', b: 'Bravo', c: 'Charlie' });
// passes
```
```js
enforce('Bravo').isNotValueOf({ a: 'Alpha', b: 'Bravo', c: 'Charlie' });
// throws
```
## isKeyOf
### Description
Determines whether a value is a key of an object
### Usage examples:
```js
enforce('bananas').isKeyOf({ bananas: 5 });
enforce(1976).isKeyOf({ 1976: 'Rocky' });
// passes
```
```js
enforce('avocados').isKeyOf({ cantelopes: 5 });
enforce(1967).isKeyOf({ 1988: 'Rain Man' });
enforce('key').isKeyOf(undefined);
enforce(15).isKeyOf(null);
enforce('star').isKeyOf(false);
enforce('triangle').isKeyOf(true);
// throws
```
## isNotKeyOf
### Description
Determines whether a value **is not** a key of an object
### Usage examples:
```js
enforce('avocados').isNotKeyOf({ cantelopes: 5 });
enforce(1967).isNotKeyOf({ 1988: 'Rain Man' });
enforce('key').isNotKeyOf(undefined);
enforce(15).isNotKeyOf(null);
enforce('star').isNotKeyOf(false);
enforce('triangle').isNotKeyOf(true);
// passes
```
```js
enforce('bananas').isNotKeyOf({ bananas: 5 });
enforce(1976).isNotKeyOf({ 1976: 'Rocky' });
// throws
```
# Failing with a message
When running enforce you can specify a custom failure message to be thrown on failure. This is done via the `message` modifier. All you need to do is add the message before the rules it refers to.
If a message is provided, it will override the default message of all rules that follow it.
```js
enforce(value)
.message('Value must be a number')
.isNumber();
.message('Value must be positive')
.isPositive();
```
# Getting Started
Vest is a validation library for forms and other flows that change over time.
It can validate only the field or step that changed, keep the results from earlier runs, and ignore an old async response when a newer one has already finished.
If you know Jest or Mocha, the authoring model will feel familiar: define a suite of named tests and use assertions to express the rules. The test-like syntax makes Vest approachable; its persistent validation runtime is what makes it different.
import GetStartedSandpack from '@site/src/components/Sandpack/GetStarted';
## The problem Vest solves
Forms are rarely validated just once. While someone fills one out:
1. The user changes one field.
2. Only the related rules should run.
3. Results for other fields should remain available.
4. Dependent fields may need to be reconsidered.
5. Async responses may arrive in the wrong order.
6. The complete workflow still needs one reliable validation result.
Vest handles the validation state without taking over your values, DOM, or components.
## Installation
```shell
npm i vest
```
## Create a suite
```js
import { create, enforce, test } from 'vest';
export const signupSuite = create((data = {}) => {
test('email', 'Email is required', () => {
enforce(data.email).isNotBlank();
});
test('username', 'Username must be at least 3 characters', () => {
enforce(data.username).longerThanOrEquals(3);
});
test('username', 'Username is already taken', async ({ signal }) => {
const response = await checkUsername(data.username, { signal });
enforce(response.available).isTruthy();
});
});
```
The suite is independent from React, Vue, Svelte, Angular, or any other UI layer. It contains the validation contract; your feature decides when to run it and how to render the result.
## Run only what changed
```js
const result = signupSuite.only('username').run(formData);
result.isPending('username');
result.hasErrors('username');
result.getError('username');
```
`suite.only('username')` runs only username tests. Results previously established for `email` and other fields remain in the suite, so `result.isValid()` still represents the complete validation picture.
Run the full suite before submission:
```js
const result = await signupSuite.run(formData);
if (result.isValid()) {
submit(formData);
}
```
When async tests exist, the returned result exposes synchronous selectors immediately and can also be awaited for final completion.
## Interactive example
This example connects a stateful Vest suite to a React form. The suite itself is framework-independent.
## Test the suite without a UI
Use `runStatic()` when a test should not inherit state from an earlier case:
```js
const passwordSuite = create(data => {
test('password', 'Password must contain at least 8 characters', () => {
enforce(data.password).longerThanOrEquals(8);
});
});
const invalid = passwordSuite.runStatic({ password: 'short' });
expect(invalid.hasErrors('password')).toBe(true);
```
The test exercises the same rules as the UI without rendering a component. Interactive application code should still use stateful `run()` so focused results can accumulate over time.
## When Vest is useful
Use Vest when validation behavior includes:
- async username, email, eligibility, inventory, or coupon checks;
- multi-step onboarding and wizards;
- fields that depend on other fields;
- optional or conditional sections;
- dynamic lists of travelers, products, or addresses;
- errors, warnings, pending states, and progressive completion;
- validation shared between browser and server.
If you only need to parse an API payload once, an Enforce schema's `.parse()` method may be enough. Attach the same schema to a Vest suite when you also need validation while the user works through a form. You can use Zod or another schema library at the boundary instead if that is already part of your stack.
## Next steps
- **[Browse the tutorials](https://vestjs.dev/docs/tutorials)**: Start with a basic suite or jump to async checks, schemas, server validation, or custom rules.
- **[See how Vest handles validation](https://vestjs.dev/docs/concepts)**: Understand what the suite remembers between runs.
- **[Async validation without stale results](https://vestjs.dev/docs/writing_tests/async_tests)**: Coordinate overlapping server checks safely.
- **[Focused updates](https://vestjs.dev/docs/writing_your_suite/focused_updates)**: Validate one field, step, or group.
- **[Dependent fields](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/include)**: Rerun related rules together.
- **[Server validation and resumption](https://vestjs.dev/docs/suite_serialization)**: Continue server validation state in the client.
# Async Validation Without Race Conditions
Async validation becomes incorrect when requests finish in a different order from the one in which they started. A slow response for an old value can otherwise replace the answer for the value currently on screen.
Vest associates async work with the run that created it. When a newer focused run supersedes an older one, the older result cannot update the current suite state.
```ts
import { create, enforce, skipWhen, test } from 'vest';
export const accountSuite = create(data => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
test('username', 'Username must contain at least 3 characters', () => {
enforce(data.username).longerThanOrEquals(3);
});
skipWhen(
result => result.hasErrors('username'),
() => {
test('username', 'Username is already taken', async ({ signal }) => {
const response = await fetch('/api/usernames/available', {
body: JSON.stringify({ username: data.username }),
headers: { 'content-type': 'application/json' },
method: 'POST',
signal,
});
const { available } = await response.json();
enforce(available).isTruthy();
});
},
);
});
```
`skipWhen` keeps the remote rule relevant while avoiding the request until the inexpensive local username rules pass.
Run only the changed field:
```ts
const result = accountSuite
.only('username')
.afterEach(() => render(accountSuite.get()))
.run(formData);
render(result); // synchronous and pending status is available immediately
await result; // resolves after the current async work completes
```
The `AbortSignal` stops obsolete network work when the transport supports cancellation. Vest's stale-result protection still guards the suite state even when the underlying request cannot be canceled.
## Submission
Focused runs are for interaction. Submission should run and await the complete suite:
```ts
const result = await accountSuite.run(formData);
if (result.isValid()) {
await submit(formData);
}
```
Debouncing can reduce request volume, but it does not replace race handling. Networks can still reorder debounced requests.
## Common mistakes
| Mistake | Correction |
| --------------------------------------------------------- | ----------------------------------------------------------- |
| Tracking request IDs only in the component | Let Vest decide which test result still has authority |
| Ignoring the test `signal` | Pass it to `fetch` or another abortable API |
| Awaiting before rendering local errors | Read the immediate result, then handle asynchronous updates |
| Using `hasErrors()` as a loading indicator | Use `isPending(field)` |
| Running the suite twice to register a completion callback | Chain `afterEach()` and `run()` on one focused suite call |
## Verify the behavior
Test with deterministic delays: start three values, finish the newest first, and finish the oldest last. The final state must still describe the newest value.
See the interactive proof on the [Vest homepage](https://vestjs.dev/) and the complete [async API reference](https://vestjs.dev/docs/writing_tests/async_tests).
# Client and Server Validation
The browser and server need different execution modes even when they share the same business rules.
- The browser uses stateful focused runs because interactions build on earlier results.
- The server uses `runStatic()` so concurrent requests never share validation state.
```ts
// browser
registrationSuite.only('email').run(formData);
// server
const result = await registrationSuite.runStatic(requestBody);
```
An API handler should await all async work before accepting data:
```ts
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
export async function register(request) {
const data = await request.json();
const result = await registrationSuite.runStatic(data);
if (!result.isValid()) {
return Response.json(
{
errors: result.getErrors(),
vestState: SuiteSerializer.serialize(result),
},
{ status: 422 },
);
}
return createAccount(data);
}
```
## Continue server state in the browser
For server-rendered workflows, serialize the full result rather than sending only an error map:
```ts
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
const result = await registrationSuite.runStatic(data);
const vestState = SuiteSerializer.serialize(result);
```
Resume it on the client:
```ts
SuiteSerializer.resume(registrationSuite, vestState);
```
The suite now knows which tests passed, failed, or were skipped on the server and can continue with focused browser runs.
## Why serialize more than errors?
An error map leaves out successful tests, warnings, group status, skipped work, and omitted branches. Serialized Vest state keeps all of that, so the browser can continue from the server result instead of rebuilding part of it by hand.
An error map also creates a second UI integration path. The application must store the server errors, route each message to the correct field component, decide how those messages interact with client errors and warnings, and clear or merge them when the user edits again.
Resuming Vest state avoids a second set of wiring. If the form already renders from the suite, the same selectors expose the server result after `SuiteSerializer.resume()`:
```ts
registrationSuite.getMessage('email');
registrationSuite.getErrors('email');
registrationSuite.getWarnings('password');
```
The UI does not need a special `serverErrors` branch. Its existing bindings keep reading from the same suite before hydration, after the server state arrives, and after subsequent focused client runs. Vest is not connecting to the components automatically; resumption makes the server result available through the validation source those components already use.
Resume only when new server state arrives. Do not resume the same payload on every render.
Add a schema when you also need to parse or transform the submitted data. See the [complete registration example](https://vestjs.dev/docs/guides/production-architecture) and [SSR serialization reference](https://vestjs.dev/docs/suite_serialization).
# Conditional Form Sections
Two conditional-field states can look similar but need different handling:
- **Not ready to run:** the rule is still required, but a prerequisite currently fails.
- **Not applicable:** the field should not count toward validity at all.
Use `skipWhen` for the first case and `omitWhen` for the second.
```ts
import { create, enforce, omitWhen, skipWhen, test } from 'vest';
export const checkoutSuite = create(data => {
test('email', 'Enter a valid email address', () => {
enforce(data.email).matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
});
skipWhen(
result => result.hasErrors('email'),
() => {
test('email', 'This email already has an account', async () => {
enforce(await isNewCustomer(data.email)).isTruthy();
});
},
);
omitWhen(!data.needsShipping, () => {
test('street', 'Street is required', () => {
enforce(data.street).isNotBlank();
});
});
});
```
When shipping is irrelevant, the omitted street test does not prevent the suite from becoming valid. When the email format is wrong, the remote email test is skipped but remains part of the eventual validity requirement.
## Keep work inside the test
`skipWhen` enters its callback so Vest can discover and preserve the skipped tests. Put network or other expensive work inside the `test` callback:
```ts
// Wrong: this work can start while the wrapper is discovering skipped tests.
skipWhen(true, () => {
startExpensiveRequest();
});
// Correct: Vest skips the test body.
skipWhen(true, () => {
test('email', 'Remote check failed', async () => {
await startExpensiveRequest();
});
});
```
`omitWhen`, by contrast, does not enter its callback when the condition is true because those tests do not belong to the current workflow at all.
## Which one should you use?
- Ask “will this rule matter later in this form?” If yes, use `skipWhen`.
- Ask “does this field or section currently exist conceptually?” If no, use `omitWhen`.
- Use `optional(field)` when one value may be blank but must satisfy its tests when provided.
Prefer `optional(field)` when one field may be blank but must pass its rules when provided. Prefer `omitWhen` when an entire branch of the workflow becomes irrelevant.
Read the [`omitWhen` reference](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/omitWhen) and [`skipWhen` reference](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/skipWhen).
# Dependent and Cross-Field Validation
A focused run needs one extra rule when fields depend on each other: changing one value must invalidate conclusions derived from it.
Use `include(...).when(...)` to express that relationship inside the suite:
```ts
import { create, enforce, include, test } from 'vest';
export const passwordSuite = create(data => {
include('confirmPassword').when('password');
test('password', 'Password must contain at least 10 characters', () => {
enforce(data.password).longerThanOrEquals(10);
});
test('confirmPassword', 'Passwords do not match', () => {
enforce(data.confirmPassword).equals(data.password);
});
});
```
Now this call validates both the changed password and its dependent confirmation:
```ts
passwordSuite.only('password').run(formData);
```
The same pattern works for start/end dates, country/postal-code rules, minimum/maximum values, and coupled configuration options.
## Avoid premature errors
Only include an untouched dependent field when it has a value or was already tested:
```ts
include('confirmPassword').when(result => {
return Boolean(data.confirmPassword) || result.isTested('confirmPassword');
});
```
This preserves dependency correctness without showing an error before the user reaches the confirmation field.
Keep the dependency next to the rules it affects. Repeating this logic in component event handlers makes it easy for different screens to validate the same data differently.
Read the complete [`include` reference](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/include).
# Validate Dynamic Lists and Arrays
Index-based validation state breaks when rows move. If the first traveler is removed, the second traveler must not inherit the first traveler's error.
Use `each()` and give every test the stable identity of its item:
```ts
import { create, each, enforce, test } from 'vest';
export const tripSuite = create(data => {
each(data.travelers, traveler => {
test(
'travelerName',
'Traveler name is required',
() => enforce(traveler.name).isNotBlank(),
`${traveler.id}:name`,
);
test(
'passportNumber',
'Passport number is required',
() => enforce(traveler.passportNumber).isNotBlank(),
`${traveler.id}:passport`,
);
});
});
```
The final `test` argument is the tracking key. It must be stable and unique to that test, so combine a database ID, UUID, or client-generated stable ID with the rule identity—not the array position.
```ts
const added = {
id: crypto.randomUUID(),
name: '',
passportNumber: '',
};
```
Run the relevant named field after editing a row. Vest reconciles keyed tests and removes state belonging to deleted items on the next run.
For item-specific rendering, keep the same stable ID in the UI and validation data. This gives React and Vest the same answer to “which logical row is this?”
Read the complete [dynamic tests reference](https://vestjs.dev/docs/writing_tests/advanced_test_features/dynamic_tests).
# Validate One Field or Step
Running every rule after each keystroke wastes work and can show errors for fields the user has not touched. With Vest, the UI says what changed and the suite keeps the other results.
```ts
const result = signupSuite.only('email').run(formData);
```
Only tests named `email` execute. Passing or failing results for fields such as `username` and `password` remain available.
## The result still contains the other fields
```ts
const data = {
bio: 'TypeScript developer',
password: 'short',
username: 'al',
};
let result = profileSuite.only('username').run(data);
result.hasErrors('username'); // true
result.isTested('password'); // false
result = profileSuite.only('password').run(data);
result.hasErrors('password'); // true
result.hasErrors('username'); // still true
data.username = 'alice';
result = profileSuite.only('username').run(data);
result.isValid('username'); // true
result.hasErrors('password'); // still true
```
The second run does not replace the result with a password-only object. Vest reconciles the new password tests with the username result already stored by the suite.
```ts
function validateField(field, nextValues) {
const result = signupSuite.only(field).run(nextValues);
return {
errors: result.getErrors(field),
pending: result.isPending(field),
tested: result.isTested(field),
};
}
```
For several fields or a workflow step, use `focus`:
```ts
signupSuite.focus({ only: ['country', 'city', 'postalCode'] }).run(formData);
signupSuite.focus({ onlyGroup: 'shipping' }).run(formData);
```
Focus belongs at the interaction call site because the UI knows what changed. Business-driven inclusion—such as a dependent field—belongs inside the suite.
## Full validation still matters
Before progressing through a guarded step, run the step group and await it. Before final submission, run the whole suite:
```ts
const step = await signupSuite.focus({ onlyGroup: 'shipping' }).run(formData);
if (step.isValidByGroup('shipping')) {
goToNextStep();
}
const complete = await signupSuite.run(formData);
```
Focused validation changes what runs now. It does not redefine the complete validity requirements.
## Common mistakes
| Mistake | Correction |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Passing only the focused field when rules read other values | Pass the current complete form object |
| Using `focus({ omit: field })` | The call-site modifier is `skip`; `omitWhen` has different suite semantics |
| Checking global `isValid()` before required fields have run | Use field or group selectors during progression and run the full suite on submit |
| Putting UI event focus inside the suite callback | Let the interaction choose `suite.only()` or `suite.focus()` |
Try adding a display name that depends on username, then focus both fields when either value changes.
Read the [focus modifier reference](https://vestjs.dev/docs/writing_your_suite/focused_updates).
# Use Vest with Form and Schema Libraries
Vest does not need to replace the other validation-related tools in an application.
| Layer | Owns |
| ------------------------------------- | -------------------------------------------------------------------------------- |
| Form manager | Values, registration, events, and submission mechanics |
| Vest suite | Focused execution, retained results, dependencies, pending work, and async races |
| Enforce schema or another schema tool | Parsing, transforming, and protecting the final submitted payload |
An Enforce schema is part of Vest, so Vest can own both validation over time and the parsed boundary. A separate schema library is optional.
## React Hook Form through Standard Schema
Vest suites implement Standard Schema. React Hook Form's official resolver package provides a Standard Schema adapter:
```ts
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { useForm } from 'react-hook-form';
import { registrationSuite } from './registrationSuite';
const form = useForm({
resolver: standardSchemaResolver(registrationSuite),
criteriaMode: 'all',
});
```
The resolver covers full-form validation. To validate as the user types or leaves a field, run the relevant field through the same suite and render the Vest result:
```ts
async function validateUsername() {
const values = form.getValues();
const result = registrationSuite.only('username').run(values);
render(result);
await result;
render(registrationSuite.get());
}
```
## Parse with Enforce
Use `enforce.shape` when you want Vest to parse the input as well as run the form's business rules:
```ts
import { create, enforce, test } from 'vest';
const registrationSchema = enforce.shape({
age: enforce.isNumeric().toNumber(),
email: enforce.isString().trim(),
});
const registrationSuite = create(data => {
test('age', 'Must be 18 or older', () => {
enforce(data.age).greaterThanOrEquals(18);
});
}, registrationSchema);
const result = await registrationSuite.runStatic(values);
if (result.isValid()) await api.register(result.value);
```
The schema parses and transforms the payload. The suite runs the remaining rules, and `result.value` contains the parsed output.
## Keep another schema library
An application that already uses Zod or another schema library can keep it at the boundary:
```ts
const result = await registrationSuite.run(values);
if (result.isValid()) {
const payload = registrationSchema.parse(values);
await api.register(payload);
}
```
Here Zod parses the application's data contract and Vest handles validation during interaction. You can replace Zod with Enforce if you would rather keep both jobs in Vest.
See the complete [production architecture](https://vestjs.dev/docs/guides/production-architecture), [Standard Schema reference](https://vestjs.dev/docs/community_resources/standard_schema), and [tool comparison](https://vestjs.dev/docs/vest_vs_the_rest).
If Vest itself should parse serialized inputs and infer the callback output, read [typed schemas and parsed results](https://vestjs.dev/docs/guides/typed-schemas).
# Reduce Repeated Async Validation
Race safety keeps old asynchronous results from corrupting current state. It does not prevent an application from starting unnecessary work. Vest provides two separate tools for reducing that work:
| Tool | Use it when | What it does |
| ------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------- |
| `memo(callback, dependencies)` | The same relevant inputs may be validated again | Restores the tests and results produced for a cached dependency set |
| `debounce(callback, delay)` | Rapid interactions should wait for a quiet period | Delays a test and keeps the latest invocation |
Neither mechanism replaces stale-result protection. Continue passing the test's `AbortSignal` to abortable work.
## Build a coupon check
```ts
import { create, enforce, test } from 'vest';
import debounce from 'vest/debounce';
import { memo } from 'vest/memo';
type CouponData = {
cartTotal: number;
coupon: string;
};
async function checkCoupon(
data: CouponData,
signal: AbortSignal,
): Promise {
const response = await fetch('/api/coupons/validate', {
body: JSON.stringify(data),
headers: { 'content-type': 'application/json' },
method: 'POST',
signal,
});
if (!response.ok) throw new Error('Coupon service failed');
const body: { valid: boolean } = await response.json();
return body.valid;
}
export const couponSuite = create((data: CouponData) => {
test('coupon', 'Coupon must contain exactly 8 characters', () => {
enforce(data.coupon).lengthEquals(8);
});
memo(
() => {
test(
'coupon',
'Coupon is not valid for this cart',
debounce(async ({ signal }) => {
enforce(await checkCoupon(data, signal)).isTruthy();
}, 300),
);
},
[data.coupon, data.cartTotal],
{ cacheSize: 10, ttl: 30_000 },
);
});
```
The dependency list contains both values that influence the remote conclusion. Reusing the coupon with a different cart total must create a different cache entry.
## Run it like any other async suite
```ts
const result = couponSuite.only('coupon').run({
cartTotal: 120,
coupon: 'SAVE0025',
});
showPending(result.isPending('coupon'));
await result;
renderErrors(result.getErrors('coupon'));
```
Debounced tests are asynchronous even when their inner callback is synchronous. Read the immediate result for pending and synchronous status, then await it or use an `afterEach` bridge for completion updates.
## Choose the smallest optimization
- Use only `debounce` when values rarely repeat but interactions arrive in bursts.
- Use only `memo` when the application revisits a small number of dependency combinations.
- Combine them when both behaviors occur and the cached answer is safe to reuse.
- Skip caching when the answer must always be fresh or is unsafe to retain. Use a short `ttl` when limited reuse is acceptable.
## Common mistakes
| Mistake | Correction |
| --------------------------------------------------------------- | ------------------------------------------------------------------- |
| Omitting a value that affects validity from the dependency list | Include every input used by the memoized block |
| Importing the removed `test.memo` API | Import `memo` from `vest/memo` |
| Importing `debounce` from the main package | Import the default export from `vest/debounce` |
| Treating debounce as race protection | Keep Vest's normal async lifecycle and pass `signal` to the request |
| Forgetting that a debounced test is pending | Render pending state and handle asynchronous completion |
Next, read [async validation without race conditions](https://vestjs.dev/docs/guides/async-validation-race-conditions) for result authority and cancellation, or the [`memo`](https://vestjs.dev/docs/writing_tests/advanced_test_features/memo) and [`debounce`](https://vestjs.dev/docs/writing_tests/advanced_test_features/debounce) references for API details.
# Validation for Multi-Step Forms
A multi-step form should validate the current step without forgetting the steps already completed. Submission still needs to check the whole form.
Model steps with groups:
```ts
import { create, enforce, group, test } from 'vest';
type Step = 'account' | 'profile' | 'billing';
type OnboardingData = {
displayName: string;
email: string;
plan: string;
};
export const onboardingSuite = create<{
fields: keyof OnboardingData;
groups: Step;
}>((data: OnboardingData) => {
group('account', () => {
test('email', 'Email is required', () => {
enforce(data.email).isNotBlank();
});
});
group('profile', () => {
test('displayName', 'Display name is required', () => {
enforce(data.displayName).isNotBlank();
});
});
group('billing', () => {
test('plan', 'Choose a plan', () => {
enforce(data.plan).isNotBlank();
});
});
});
```
The explicit config types are optional, but they make field and group names discoverable and reject misspelled step names in TypeScript.
Guard the next-step action with a focused, awaited run:
```ts
async function canContinue(step: Step, data: OnboardingData) {
const result = await onboardingSuite.focus({ onlyGroup: step }).run(data);
return result.isValidByGroup(step);
}
```
Earlier step results remain in the suite, so a review screen can show the whole form without repeating every remote check.
Cross-field rules remain ordinary TypeScript. Put password and confirmation tests in the same group, then run that group when either value changes. If a field outside the active group must also rerun, use the dependent-field patterns described in [dependent and cross-field validation](https://vestjs.dev/docs/guides/dependent-fields).
## Final submission
```ts
const result = await onboardingSuite.run(data);
if (!result.isValid()) {
navigateToFirstInvalidStep(result);
return;
}
```
Do not equate “current step has no errors” with “complete workflow is valid.” Use group selectors for step navigation and whole-suite selectors for submission.
`onlyGroup` excludes ungrouped top-level tests. Put step requirements inside their groups or run deliberate top-level checks separately.
Read more about [grouping tests](https://vestjs.dev/docs/writing_tests/advanced_test_features/grouping_tests).
# Next.js Server Actions Without Double Validation
A Server Action must validate every request independently. When it returns errors, the browser should not need to rerun expensive rules merely to reconstruct the same validation state.
## Server Action
This example uses a `registrationSuite` created with an Enforce schema, so a successful result contains the parsed input in `result.value`.
```ts
'use server';
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
import { registrationSuite } from './registrationSuite';
export async function register(data) {
const result = await registrationSuite.runStatic(data);
if (!result.isValid()) {
return {
ok: false,
errors: result.getErrors(),
vestState: SuiteSerializer.serialize(result),
};
}
await saveAccount(result.value);
return { ok: true };
}
```
`runStatic()` is important: a module-level suite may serve many concurrent requests, and server requests must not merge state with one another.
## Resume on the client
```tsx
'use client';
import { useEffect } from 'react';
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
import { registrationSuite } from './registrationSuite';
export function RegistrationForm({ actionState }) {
useEffect(() => {
if (actionState?.vestState) {
SuiteSerializer.resume(registrationSuite, actionState.vestState);
}
}, [actionState?.vestState]);
// suite.get() now contains the server's complete validation result.
}
```
Resuming restores more than an error map: the suite knows which rules passed, failed, or were skipped. The next browser interaction can run one field while keeping the other server results.
## Add a parsing boundary
Parse action input before saving it. You can use an Enforce schema directly through `.parse()` or attach it to the Vest suite and call `runStatic()`. If the application already uses Zod, Valibot, or another schema tool, it can stay in that role.
The reference implementation is in the [production registration example](https://vestjs.dev/docs/guides/production-architecture). See [suite serialization](https://vestjs.dev/docs/suite_serialization) for the lower-level API.
import ProductionRegistrationSandpack from '@site/src/components/Sandpack/ProductionRegistration';
# Complete Registration Example
This example splits the work between three pieces:
> **The form layer owns the inputs, the schema owns the boundary, and Vest owns how validation evolves.**
The executable source lives in [`examples/production-registration`](https://github.com/ealush/vest/tree/latest/examples/production-registration).
## What is in the example
- React Hook Form owns values, registration, and submission mechanics.
- The official Standard Schema resolver connects full-form submission to Vest.
- Vest runs one field or one workflow step while retaining earlier results.
- Username availability is asynchronous and race-safe.
- Password confirmation is linked to password changes.
- Business accounts reveal a conditional company section.
- Password strength is a non-blocking warning.
- The server uses a stateless Vest run.
- This implementation uses Zod for the final API boundary; an Enforce schema can own the same boundary through `.parse()` or as the suite schema.
- A runnable Vite page demonstrates the architecture with deterministic local services.
- Vitest proves focused retention, async race behavior, conditionals, warnings, and server isolation.
## Try it
The playground loads its files from the example project, so the code shown here and the tested example stay in sync. Try `taken` as the username and change it before the delayed response returns. You can also switch to a business account or submit a weak password to see the conditional fields and warning state.
The server handler and Server Action remain in the standalone project because a browser sandbox cannot faithfully demonstrate request isolation, HTTP failures, or secret handling. Those paths stay covered by the example's Vitest suite.
## Request flow
```text
Field interaction
→ suite.only(field).run(values)
→ render tested / pending / error / warning state
Step navigation
→ suite.focus({ onlyGroup: step }).run(values)
→ await async work
→ continue only when the group is valid
Submission
→ React Hook Form awaits Vest through the Standard Schema resolver
→ boundarySchema.parse(values)
→ POST parsed values
→ server boundarySchema.parse(requestBody)
→ server suite.runStatic(parsedValues)
→ persist parsed payload
```
## Why the example also uses Zod
Many applications already use Zod, so this example shows how to keep it. Zod parses the submitted payload; Vest handles focused runs, async checks, warnings, conditional fields, and server-side business rules.
Zod is optional here. An Enforce schema created with `enforce.shape` can parse and transform the payload with `.parse()`. You can also pass it to `create`; then `run()` and `runStatic()` expose the parsed value as `result.value`.
## Run the example
```shell
yarn workspace @vest/example-production-registration test
yarn workspace @vest/example-production-registration typecheck
yarn workspace @vest/example-production-registration build
yarn workspace @vest/example-production-registration dev
```
Start with the [suite source](https://github.com/ealush/vest/blob/latest/examples/production-registration/src/registrationSuite.ts), then inspect the [React form](https://github.com/ealush/vest/blob/latest/examples/production-registration/src/RegistrationForm.tsx) and [server handler](https://github.com/ealush/vest/blob/latest/examples/production-registration/src/server.ts).
# Typed Schemas and Parsed Results
An Enforce schema can parse the input before the suite's tests run. Pass it as the second argument to `create`; the suite callback receives the parsed type and a successful result exposes the parsed value.
## Define input and output together
```ts
import { create, enforce, test } from 'vest';
const registrationSchema = enforce.shape({
age: enforce.isNumeric().toNumber(),
newsletter: enforce.isBoolean(),
username: enforce.isString().trim(),
});
export const registrationSuite = create(data => {
// `data` is parsed output: age is a number and username is trimmed.
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
test('age', 'You must be at least 18', () => {
enforce(data.age).greaterThanOrEquals(18);
});
}, registrationSchema);
```
The first rule in a parser chain determines the accepted input type. The final parser determines the output type. Here, `age` accepts a numeric string or number, while the suite callback receives a number.
## Read the parsed result
```ts
const result = registrationSuite.run({
age: '36',
newsletter: true,
username: ' Ada ',
});
if (result.isValid()) {
result.value;
// { age: 36, newsletter: true, username: 'Ada' }
}
```
Use `result.value` only after a successful result. Invalid and pending results do not expose a valid parsed output.
## Keep the execution APIs distinct
| Situation | API |
| ------------------------------------------ | ---------------------------------------- |
| Interactive validation with retained state | `registrationSuite.run(data)` |
| Independent server request | `registrationSuite.runStatic(data)` |
| Compatible library expects Standard Schema | Pass `registrationSuite` to that library |
Standard Schema consumers invoke the suite's `~standard.validate` hook. Application code should continue using `run()` or `runStatic()` because those return Vest's full result with warnings, pending state, groups, focus, and selectors.
When implementing or testing a Standard Schema adapter directly, the contract is available explicitly:
```ts
const output = await registrationSuite['~standard'].validate({
age: '36',
newsletter: true,
username: ' Ada ',
});
```
Successful Standard Schema output contains the parsed values, so `output.value.age` is a number.
## Using another schema library
Enforce can parse your API boundary, but it does not have to. If your application already uses Zod, Valibot, or another schema library there, keep it and use Vest for validation during interaction.
Read [form and schema library integration](https://vestjs.dev/docs/guides/form-and-schema-integration), the [Standard Schema reference](https://vestjs.dev/docs/community_resources/standard_schema), and the [production registration architecture](https://vestjs.dev/docs/guides/production-architecture) for complete compositions.
# Errors, Warnings, Pending, and Untested State
Interactive validation has more than two states. A field may be untested, pending, passing, failing with a blocking error, or failing with a non-blocking warning.
| State | What it means | Typical UI |
| -------- | -------------------------------------------------- | ------------------------- |
| Untested | The field has not been checked yet | Neutral guidance |
| Pending | Asynchronous work for the current value is running | Progress indicator |
| Error | A blocking rule failed | Error message |
| Warning | Advisory guidance failed | Non-blocking message |
| Passing | Current tested rules passed | Optional success feedback |
```ts
const result = suite.only('username').run(data);
const view = {
tested: result.isTested('username'),
pending: result.isPending('username'),
errors: result.getErrors('username'),
warnings: result.getWarnings('username'),
valid: result.isValid('username'),
};
```
Do not render an error merely because `isValid(field)` is false. An untested or pending field can be incomplete without containing a user error.
Use warnings for guidance that should not block submission:
```ts
import { enforce, test, warn } from 'vest';
test('password', 'Add a number for a stronger password', () => {
warn();
enforce(data.password).matches(/\d/);
});
```
If warning severity depends on an asynchronous answer, capture `useWarn()` before the first `await` and call its setter while the test is still running:
```ts
import { enforce, test, useWarn } from 'vest';
test('username', 'This username is very common', async () => {
const markAsWarning = useWarn();
const common = await isCommonUsername(data.username);
if (common) markAsWarning();
enforce(common).isFalsy();
});
```
Calling `useWarn()` for the first time after an `await` has lost the active test context. Capture the setter synchronously when you need to decide the severity later.
Render status in this order:
1. Untested: neutral guidance.
2. Pending: progress feedback.
3. Error: blocking feedback.
4. Warning: advisory feedback.
5. Passing: success feedback when useful.
On submission, await the complete suite and require `isValid()`. Warnings do not make the suite invalid; errors, pending work, and incomplete required tests do.
Keep warnings separate from errors. That is what lets the UI show advice without blocking submission.
Read the [result selector reference](https://vestjs.dev/docs/writing_your_suite/accessing_the_result) and [warning guide](https://vestjs.dev/docs/writing_tests/warn_only_tests).
# When Not to Use Vest
Vest is most useful when validation changes during user interaction. It is more machinery than you need for every value check.
## Use native HTML validation when
- the form has only a few independent fields;
- `required`, `min`, `max`, `pattern`, and input types express the rules;
- custom error timing and server rule sharing are unnecessary.
## Use only a schema API when
- the primary job is parsing an API request, configuration file, or environment variables;
- validation happens once at a clear boundary;
- transformations, codecs, JSON Schema, or ecosystem-specific type integrations are more important than interaction state.
An Enforce schema's `.parse()` API can be enough for this job without creating a Vest suite. Zod, Valibot, ArkType, Joi, and Ajv also have strengths in this category.
## Use a form manager's built-in validation when
- the workflow is tied to one UI framework;
- its field-level rules and error state cover the complete UX;
- rules do not need to run independently on the server or in another interface.
## Choose Vest when the hard part is behavior over time
Vest earns its place when several of these are true:
- only the changed field or step should run;
- earlier results must remain available;
- async checks overlap and stale responses are dangerous;
- fields depend on other fields;
- conditional sections enter and leave the validity model;
- warnings must not block submission;
- the same business rules run in browser and server workflows.
## Combine tools when responsibilities differ
You might use React Hook Form for input handling and Vest with Enforce for both interactive validation and the API boundary. If the project already uses Zod, keep Zod at the boundary and use Vest for the form. Either approach works.
See the [production architecture](https://vestjs.dev/docs/guides/production-architecture) and [comparison guide](https://vestjs.dev/docs/vest_vs_the_rest).
import HonoIntegration from '@site/src/components/Sandpack/HonoIntegration';
# Vest with Hono
Hono's Standard Validator middleware accepts a Vest suite directly. The local example executes in-memory requests, rejects invalid JSON before the handler, and passes parsed output to valid handlers.
## Installation
```shell
npm install vest hono @hono/standard-validator
```
The compatibility workspace pins Hono 4.13.0; @hono/standard-validator 0.3.0 and imports only public package entry points.
## Runnable demonstration
The playground loads its tested source directly from the [local compatibility workspace](https://github.com/ealush/vest/tree/latest/integrations/hono). Edit the request JSON and use the file tabs to inspect the middleware, Vest suite, and React client.
## Tested versions
- Vest 6.3.2
- Hono 4.13.0; @hono/standard-validator 0.3.0
## Proven capabilities
- asynchronous
- input inference
- multiple issues
- nested paths
- output inference
- synchronous
- transformed output
Runtime tests, compile-time inference tests, and the browser build run through `yarn integrations:verify`.
## Known limitations
- Standard Schema middleware validates complete requests and does not expose Vest stateful interaction features.
## Upstream status
No upstream change has been created. This page and the local workspace are the Vest-owned proof.
# Ecosystem Integrations
Every listed integration is backed by a private local workspace that imports the real consumer package, runs runtime and type checks, and builds its documented example. An upstream contribution is proposed only after the Vest-side proof is green.
Standard Schema entries prove full-payload interoperability. Focused execution, retained state, and race coordination are listed only when an integration directly exercises Vest's native Suite Object API.
## Specifications and interoperability
| Integration | Status | Tested versions | Mechanism | Runtime | Types | Demo | Upstream |
| ----------------------------------------------------- | ----------- | --------------------------------------- | --------------- | ------- | ----- | ---- | ----------------------------------------------------------------- |
| [Standard Schema](https://vestjs.dev/docs/integrations/standard-schema) | upstream-pr | Vest 6.3.2; @standard-schema/spec 1.0.0 | standard-schema | ✅ | ✅ | ✅ | [PR](https://github.com/standard-schema/standard-schema/pull/177) |
## Form state and framework forms
| Integration | Status | Tested versions | Mechanism | Runtime | Types | Demo | Upstream |
| ------------------------------------------------- | ---------- | --------------------------------------- | -------------------- | ------- | ----- | ---- | ---------- |
| [TanStack Form](https://vestjs.dev/docs/integrations/tanstack-form) | docs-green | Vest 6.3.2; @tanstack/react-form 1.33.3 | native-plus-standard | ✅ | ✅ | ✅ | Not opened |
## Server and APIs
| Integration | Status | Tested versions | Mechanism | Runtime | Types | Demo | Upstream |
| ------------------------------- | ---------- | ------------------------------------------------------- | --------------- | ------- | ----- | ---- | ---------- |
| [Hono](https://vestjs.dev/docs/integrations/hono) | docs-green | Vest 6.3.2; Hono 4.13.0; @hono/standard-validator 0.3.0 | standard-schema | ✅ | ✅ | ✅ | Not opened |
| [tRPC](https://vestjs.dev/docs/integrations/trpc) | docs-green | Vest 6.3.2; @trpc/server 11.18.0 | standard-schema | ✅ | ✅ | ✅ | Not opened |
## Routers and actions
| Integration | Status | Tested versions | Mechanism | Runtime | Types | Demo | Upstream |
| ----------------------------------------------------- | ---------- | ------------------------------------------- | --------------- | ------- | ----- | ---- | ---------- |
| [TanStack Router](https://vestjs.dev/docs/integrations/tanstack-router) | docs-green | Vest 6.3.2; @tanstack/react-router 1.170.18 | standard-schema | ✅ | ✅ | ✅ | Not opened |
## Configuration
| Integration | Status | Tested versions | Mechanism | Runtime | Types | Demo | Upstream |
| ----------------------------------- | ---------- | ------------------------------------ | --------------- | ------- | ----- | ---- | ---------- |
| [T3 Env](https://vestjs.dev/docs/integrations/t3-env) | docs-green | Vest 6.3.2; @t3-oss/env-core 0.13.11 | standard-schema | ✅ | ✅ | ✅ | Not opened |
## Data and transport
_No local integration has been registered in this category yet._
# Vest with Standard Schema
Standard Schema lets validation consumers invoke a Vest suite or an Enforce schema through a shared interface. This integration uses full-payload Standard Schema validation; it does not substitute for Vest's native focused, retained-state workflow APIs.
## Installation
```shell
npm install vest
```
The compatibility workspace pins @standard-schema/spec 1.0.0 and imports only public package entry points.
## Implementation example
This source is exercised by the runtime and compile-time checks in the [local compatibility workspace](https://github.com/ealush/vest/tree/latest/integrations/standard-schema).
```ts
import { create, enforce, mode, Modes, test } from 'vest';
export const accountSchema = enforce.shape({
email: enforce.isString().trim().toLower(),
profile: enforce.shape({
age: enforce.isNumeric().toNumber(),
name: enforce.isString().trim(),
}),
});
export type AccountInput = Parameters[0];
export type AccountOutput = ReturnType;
export const accountSuite = create(data => {
mode(Modes.ALL);
test('profile.name', 'Name must contain at least 2 characters', () => {
enforce(data.profile.name.trim()).longerThanOrEquals(2);
});
test('email', 'Email must contain an @ sign', () => {
enforce(data.email.trim().toLowerCase()).matches(/@/);
});
test('email', 'Email must use the example.com domain', () => {
enforce(data.email.trim().toLowerCase()).endsWith('@example.com');
});
}, accountSchema);
export const usernameSchema = enforce.shape({
username: enforce.isString().trim(),
});
export type UsernameInput = Parameters[0];
export type UsernameOutput = ReturnType;
export function createUsernameSuite(
isAvailable: (username: string) => Promise,
) {
return create(data => {
test('username', 'Username is already taken', async () => {
enforce(await isAvailable(data.username)).isTruthy();
});
}, usernameSchema);
}
```
## Tested versions
- Vest 6.3.2
- @standard-schema/spec 1.0.0
## Proven capabilities
- asynchronous
- input inference
- multiple issues
- nested paths
- output inference
- synchronous
- transformed output
Runtime tests, compile-time inference tests, and the browser build run through `yarn integrations:verify`.
## Known limitations
- Standard Schema runs complete validation and does not expose focused execution, retained state, warnings, groups, or race coordination.
- Enforce schemas identify their Standard Schema vendor as n4s; Vest suites identify their vendor as vest.
## Upstream status
Tracked in [standard-schema/standard-schema PR](https://github.com/standard-schema/standard-schema/pull/177).
# Vest with T3 Env
T3 Env accepts Standard Schema validators for individual environment variables. Vest's Enforce schemas validate server and client configuration and return parsed values such as a numeric port.
## Installation
```shell
npm install vest @t3-oss/env-core
```
The compatibility workspace pins @t3-oss/env-core 0.13.11 and imports only public package entry points.
## Implementation example
This source is exercised by the runtime and compile-time checks in the [local compatibility workspace](https://github.com/ealush/vest/tree/latest/integrations/t3-env).
```ts
import { createEnv } from '@t3-oss/env-core';
import { enforce } from 'vest';
import 'vest/isURL';
export function parseEnvironment(
runtimeEnv: Record,
) {
return createEnv({
client: {
PUBLIC_APP_NAME: enforce.isString().trim(),
},
clientPrefix: 'PUBLIC_',
emptyStringAsUndefined: true,
isServer: true,
onValidationError: issues => {
throw new Error('Invalid environment', { cause: issues });
},
runtimeEnv,
server: {
API_URL: enforce.isURL(),
PORT: enforce.isNumeric().toNumber(),
},
});
}
export function readClientEnvironment(
runtimeEnv: Record,
) {
const env = createEnv({
client: { PUBLIC_APP_NAME: enforce.isString().trim() },
clientPrefix: 'PUBLIC_',
isServer: false,
runtimeEnv,
server: { SECRET: enforce.isString() },
});
return {
appName: env.PUBLIC_APP_NAME,
readSecret: () => env.SECRET,
};
}
```
## Tested versions
- Vest 6.3.2
- @t3-oss/env-core 0.13.11
## Proven capabilities
- input inference
- multiple issues
- output inference
- synchronous
- transformed output
Runtime tests, compile-time inference tests, and the browser build run through `yarn integrations:verify`.
## Known limitations
- T3 Env validates each variable independently, so Enforce schemas are a more natural fit than a stateful Vest suite.
- Environment parsing is a one-shot configuration boundary rather than an interactive workflow.
## Upstream status
No upstream change has been created. This page and the local workspace are the Vest-owned proof.
import TanStackFormIntegration from '@site/src/components/Sandpack/TanStackFormIntegration';
# Vest with TanStack Form
TanStack Form owns field state while an instance-owned Vest suite provides focused change validation and Standard Schema submission validation.
## Installation
```shell
npm install vest @tanstack/react-form
```
The compatibility workspace pins @tanstack/react-form 1.33.3 and imports only public package entry points.
## Runnable demonstration
The playground loads its tested source directly from the [local compatibility workspace](https://github.com/ealush/vest/tree/latest/integrations/tanstack-form). Edit either field and use the file tabs to inspect focused change validation and full-form submission validation.
## Tested versions
- Vest 6.3.2
- @tanstack/react-form 1.33.3
## Proven capabilities
- focused execution
- input inference
- multiple issues
- nested paths
- retained state
- synchronous
Runtime tests, compile-time inference tests, and the browser build run through `yarn integrations:verify`.
## Known limitations
- TanStack Form's generic Standard Schema submission validation does not expose Vest's focused execution or retained state; the field validators use the Suite Object API directly.
- TanStack Form validates Standard Schema output but does not forward transformed output to the submit callback.
## Upstream status
No upstream change has been created. This page and the local workspace are the Vest-owned proof.
import TanStackRouterIntegration from '@site/src/components/Sandpack/TanStackRouterIntegration';
# Vest with TanStack Router
TanStack Router accepts a Vest Enforce schema directly as a Standard Schema search-parameter validator. The route rejects invalid URLs and exposes Vest's parsed output as its inferred search type.
## Installation
```shell
npm install vest @tanstack/react-router
```
The compatibility workspace pins @tanstack/react-router 1.170.18 and imports only public package entry points.
## Runnable demonstration
The playground loads its tested source directly from the [local compatibility workspace](https://github.com/ealush/vest/tree/latest/integrations/tanstack-router). Edit the URL and use the file tabs to inspect search validation and inferred parsed values.
## Tested versions
- Vest 6.3.2
- @tanstack/react-router 1.170.18
## Proven capabilities
- input inference
- output inference
- synchronous
- transformed output
Runtime tests, compile-time inference tests, and the browser build run through `yarn integrations:verify`.
## Known limitations
- TanStack Router requires search validation to be synchronous.
- Search parsing is one-shot boundary validation and does not expose Vest stateful interaction features.
## Upstream status
No upstream change has been created. This page and the local workspace are the Vest-owned proof.
# Vest with tRPC
tRPC accepts Standard Schema validators in its procedure input parser. This example uses a real router and in-process caller, so invalid data is rejected before the procedure and Vest's parsed output reaches valid procedures.
## Installation
```shell
npm install vest @trpc/server
```
The compatibility workspace pins @trpc/server 11.18.0 and imports only public package entry points.
## Implementation example
This source is exercised by the runtime and compile-time checks in the [local compatibility workspace](https://github.com/ealush/vest/tree/latest/integrations/trpc).
```ts
import { initTRPC, type inferRouterInputs } from '@trpc/server';
import { create, enforce, test } from 'vest';
const accountSchema = enforce.shape({
email: enforce.isString().trim().toLower(),
profile: enforce.shape({ age: enforce.isNumeric().toNumber() }),
});
const usernameSchema = enforce.shape({
username: enforce.isString().trim(),
});
export const accountSuite = create(data => {
test('profile.age', 'Must be at least 18', () => {
enforce(Number(data.profile.age)).greaterThanOrEquals(18);
});
test('email', 'Use an example.com email', () => {
enforce(data.email.trim().toLowerCase()).endsWith('@example.com');
});
}, accountSchema);
const trpc = initTRPC.create();
export const appRouter = trpc.router({
createAccount: trpc.procedure.input(accountSuite).mutation(({ input }) => ({
account: input,
accepted: true as const,
})),
});
export type AppRouter = typeof appRouter;
export async function createAccount(
input: inferRouterInputs['createAccount'],
) {
return appRouter.createCaller({}).createAccount(input);
}
export function createAsyncRouter(
isAvailable: (username: string) => Promise,
) {
const usernameSuite = create(data => {
test('username', 'Username is already taken', async () => {
enforce(await isAvailable(data.username)).isTruthy();
});
}, usernameSchema);
return trpc.router({
reserveUsername: trpc.procedure
.input(usernameSuite)
.mutation(({ input }) => input),
});
}
```
## Tested versions
- Vest 6.3.2
- @trpc/server 11.18.0
## Proven capabilities
- asynchronous
- input inference
- multiple issues
- nested paths
- output inference
- synchronous
- transformed output
Runtime tests, compile-time inference tests, and the browser build run through `yarn integrations:verify`.
## Known limitations
- Procedure input parsing validates complete payloads and does not expose Vest stateful interaction features.
- tRPC wraps Standard Schema failures in a BAD_REQUEST error.
## Upstream status
No upstream change has been created. This page and the local workspace are the Vest-owned proof.
import AnyTestRecipeSandpack from '@site/src/components/Sandpack/AnyTestRecipe';
# "any" Test - At least one test must pass
A common scenario is to require at least one test to pass to consider the suite as passing. For example, you might require just one checkbox to be checked.
While "product-wise" you say "at-least-one", technically, there's nothing binding these checkboxes together. So, if there's nothing binding them technically, Vest can't be aware of that requirement without you telling it about it. Let's try to understand what it is that we're trying to do in "vest" terms.
In our case, the following logic should apply - Each of the checkboxes can be optional, if any of the other checkboxes is checked, or more specifically, since Vest has no notion of "checked" - Each of the checkboxes tests can be optional, if any of the other checkboxes tests are valid.
Vest has the [optional](https://vestjs.dev/docs/writing_your_suite/optional_fields) function, which allows you to specify just that. You can specify a condition in which a test can be omitted from the results.
Our code might look somewhat like this:
## Live example
```js
import { create, test, enforce, only, optional } from 'vest';
const suite = create((data = {}, currentField) => {
only(currentField);
optional({
chk_a: () => suite.get().isValid('chk_b') || suite.get().isValid('chk_c'),
chk_b: () => suite.get().isValid('chk_a') || suite.get().isValid('chk_c'),
chk_c: () => suite.get().isValid('chk_a') || suite.get().isValid('chk_b'),
});
// Alternatively, this is less verbose, but a duplicate of your test logic.
// You can choose what's best for you.
// optional({
// chk_a: () => data.chk_b || data.chk_c,
// chk_b: () => data.chk_a || data.chk_c,
// chk_c: () => data.chk_a || data.chk_b
// });
test('chk_a', () => {
enforce(data.chk_a).isTruthy();
});
test('chk_b', () => {
enforce(data.chk_b).isTruthy();
});
test('chk_c', () => {
enforce(data.chk_c).isTruthy();
});
});
export default suite;
```
# `focus({ skip / skipGroup / onlyGroup })` Patterns
This recipe clarifies a common source of confusion:
- `skip` excludes by **field name**.
- `skipGroup` excludes by **group name**.
## Mental model
Use `skip` when the question is **"which field(s) should not run?"**.
Use `skipGroup` when the question is **"which validation section should not run?"**.
## Pattern 1: Skip one field everywhere
```js
suite.focus({ skip: 'email' }).run(formData);
```
This skips every `test('email', ...)`, regardless of whether it is top-level or nested in groups.
## Pattern 2: Skip one entire section
```js
suite.focus({ skipGroup: 'signUp' }).run(formData);
```
This skips all tests declared inside `group('signUp', ...)`, regardless of field names.
## Pattern 3: Validate one field and skip heavy checks
```js
suite
.focus({ only: 'username', skipGroup: 'availabilityChecks' })
.run(formData);
```
Useful for blur validation in forms where async checks are expensive.
## Pattern 4: Wizard step validation
```js
// run only step2 validations by skipping other groups
suite.focus({ skipGroup: ['step1', 'step3'] }).run(formData);
```
## Pattern 5: Combine both scopes
```js
suite.focus({ skip: 'password', skipGroup: 'billing' }).run(formData);
```
This skips all `password` tests globally and all tests in `billing` group.
## Pattern 6: Validating a Group's Fields only (`onlyGroup`)
```js
// Validate ONLY the 'step1' group.
// All other groups AND top-level tests are skipped.
suite.focus({ onlyGroup: 'step1' }).run(formData);
```
## See also
- [Focused Updates](https://vestjs.dev/docs/writing_your_suite/focused_updates)
- [Including and Excluding Fields](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/skip_and_only)
# Recipe- validating typescript enums
Sometimes you might want to validate that a value is one of the keys or values of a typescript enum. Since typescript enums are compiled to objects, you can use the `inside` function to validate that the value is one of the keys or values of the enum.
```ts
enum Fruits {
APPLE = 'apple',
BANANA = 'banana',
CANTELOPE = 'cantelope',
}
// ...
// If you need the enum by key:
test('fruit', 'fruit is a key of fruits enum', () => {
// data.fruit is a key of ["APPLE", "BANANA", "CANTELOPE"]
enforce(data.fruit).inside(Object.keys(Fruits));
});
// If you need the enum by value:
test('fruit', 'fruit is a value of fruits enum', () => {
// data.fruit is a value of ["apple", "banana", "cantelope"]
enforce(data.fruit).inside(Object.values(Fruits));
});
```
# Server-Side Validations
Vest is isomorphic and runs in Node.js environments.
## Stateless Runs
On the server, you typically want a stateless validation run - one that doesn't remember the previous state of fields.
In Vest, use the `.runStatic()` method. Each call produces a fresh result object without merging into prior state.
```javascript
import { create, test, enforce } from 'vest';
const suite = create(data => {
test('username', 'Required', () => {
enforce(data.username).isNotBlank();
});
});
export default suite;
```
**Usage in API Handler:**
```javascript
import suite from './validation';
app.post('/user', (req, res) => {
// runStatic guarantees a fresh, stateless result every time
const result = suite.runStatic(req.body);
if ('then' in result) {
// If async tests exist, wait for completion
return result.then(finalResult => {
if (finalResult.hasErrors()) {
return res.status(400).json(finalResult.getErrors());
}
// ...
return res.sendStatus(204);
});
}
if (result.hasErrors()) {
return res.status(400).json(result.getErrors());
}
// ...
// ...
});
```
## SSR & Hydration
When using Vest with Server-Side Rendering (SSR) frameworks (like Next.js, Remix, or Nuxt), you often want to validate on the server, send the validation state to the client, and resume without rerunning everything.
Use `SuiteSerializer.serialize` to serialize the validation result state and `SuiteSerializer.resume` on the client to hydrate it.
### Server Side
```javascript
// server.js
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
import suite from './suite';
export async function action({ request }) {
const formData = await request.formData();
const data = Object.fromEntries(formData);
// Run validation
const result = suite.runStatic(data);
// Check for errors
if (result.hasErrors()) {
return json({
errors: result.getErrors(),
// Serialize the result state to send to the client
vestState: SuiteSerializer.serialize(result),
});
}
}
```
### Client Side
On the client, hydrate the suite with the state received from the server using `SuiteSerializer.resume()`.
```javascript
// client.js
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
import suite from './suite';
export function MyForm({ actionData }) {
// Resume the suite state if provided by the server
if (actionData?.vestState) {
SuiteSerializer.resume(suite, actionData.vestState);
}
// ... render form
}
```
Once resumed, `suite.hasErrors()`, `suite.isValid()`, and other selectors will return data reflecting the server-side run. Subsequent calls to `suite.run()` on the client will update this state normally.
For a deep dive into this pattern, read **[Server-Side Rendering & Hydration](https://vestjs.dev/docs/suite_serialization)**.
# Server-Side Rendering (SSR) & Hydration
Modern frameworks like Next.js, Remix, and SvelteKit often validate forms on the server before sending a response. A common problem is "Double Validation": running tests on the server, showing errors, but then forcing the client-side library to re-run everything from scratch to "know" about those errors.
Vest 6 solves this with **State Serialization**. You can take the state of a suite calculated on the server and "inject" it into the client-side suite.
## The `SuiteSerializer`
To keep the API surface clean, serialization tools are grouped under the `SuiteSerializer` export.
```javascript
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
```
### `SuiteSerializer.serialize(result)`
Takes a result object (for example, from `suite.runStatic(data)`) and returns a serializable string (safe for JSON transport).
### `SuiteSerializer.resume(suite, serializedData)`
Takes a suite instance and serialized data, and applies that state to the suite.
## Complete Workflow Example
Imagine a Remix or Next.js action handling a form submission.
### 1. Server Side
Run the validation. If it fails, send the serialized state back to the frontend.
```javascript
// server-action.js
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
import suite from './validation';
export async function action(formData) {
// 1. Run validation
// We use runStatic and serialize that result for hydration
const result = suite.runStatic(formData);
if (result.hasErrors()) {
return {
success: false,
errors: result.getErrors(),
// 2. Serialize the server result state
vestState: SuiteSerializer.serialize(result),
};
}
// ... handle success
}
```
### 2. Client Side
When your component mounts or receives the action data, resume the suite.
```javascript
// registration-form.jsx
import { useEffect } from 'react';
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
import suite from './validation';
export function RegistrationForm({ actionData }) {
// 3. Resume state if it exists
if (actionData?.vestState) {
SuiteSerializer.resume(suite, actionData.vestState);
}
// suite.get() now immediately reflects the server errors!
// suite.hasErrors('username') will be true without running logic.
return ;
}
```
:::caution Why not just pass the errors?
You might wonder, "Why not just pass the error object?"
If you only pass errors, your suite doesn't know _which_ tests passed, which are pending, or which groups were skipped. Resuming the full state lets the next field edit continue from the server result without rebuilding that context in the client.
:::
# Vest Tutorials
Start with a small suite, or jump straight to the problem you are trying to solve. The examples cover focused validation, async checks, conditional fields, schemas, and sharing validation between the browser and server.
One API detail is worth knowing before you begin:
1. Use `suite.run(data)` for stateful application validation.
2. Use `suite.runStatic(data)` for an independent server request or isolated execution.
3. Pass the suite itself to a library that supports Standard Schema. The `~standard.validate` hook is for those integrations, not for calling directly in application code.
## Pick a tutorial
| Tutorial | Example | What it covers |
| ---------------------------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------- |
| [Write your first suite](https://vestjs.dev/docs/get_started) | Signup | `create`, `test`, `enforce`, and result selectors |
| [Validate one field without forgetting the rest](https://vestjs.dev/docs/guides/focused-validation) | Profile form | Focused runs and retained results |
| [Handle async checks safely](https://vestjs.dev/docs/guides/async-validation-race-conditions) | Username availability | Pending state, cancellation, and race safety |
| [Avoid repeated async work](https://vestjs.dev/docs/guides/memo-and-debounce) | Coupon check | `memo`, `debounce`, and `AbortSignal` |
| [Show warnings without blocking submission](https://vestjs.dev/docs/guides/validation-status) | Password strength | Errors, warnings, pending, and untested state |
| [Skip or remove conditional rules](https://vestjs.dev/docs/guides/conditional-sections) | Pickup and delivery | `skipWhen`, `omitWhen`, and `optional` |
| [Validate a multi-step form](https://vestjs.dev/docs/guides/multi-step-workflows) | Onboarding | Groups, step validity, and cross-field rules |
| [Parse input and keep the typed result](https://vestjs.dev/docs/guides/typed-schemas) | Registration | Parsing, type inference, and Standard Schema |
| [Share validation between server and client](https://vestjs.dev/docs/guides/client-server-validation) | Server validation and browser resume | `runStatic`, serialization, and resumption |
| [Write rules for your own domain](https://vestjs.dev/docs/enforce/creating_custom_rules) | Custom Enforce rules | `condition`, `enforce.extend`, and sibling context |
## A good order for learning Vest
If Vest is new to you, read the first three in order. They introduce suites, focused runs, and async validation—the ideas most of the other guides build on. After that, choose the examples that match your application.
## See everything working together
- Open the [complete registration example](https://vestjs.dev/docs/guides/production-architecture) for a runnable React application.
- Read [Vest, schema validators, and form libraries](https://vestjs.dev/docs/vest_vs_the_rest) to decide which tool should handle each part of your form.
- Check [when not to use Vest](https://vestjs.dev/docs/guides/when-not-to-use-vest) if your validation is simple or only runs once.
import { SchemaTypedSandpack, ConfigTypedSandpack } from '@site/src/components/Sandpack/TypedSuites';
# TypeScript Support
Vest is written fully in TypeScript and provides first-class typing for suites, schemas, and custom rules. There are four ways to create a typed suite, each offering a different level of control.
## Schema-Aware Suite Creation
Passing an `n4s` schema as the second argument to `create` automatically types your suite callback, `.run()` calls, and all suite-level APIs. The schema is enforced at runtime and the suite result records the validated `input` and `output` in `result.types`.
```typescript
import { create, test, enforce } from 'vest';
const userSchema = enforce.shape({
username: enforce.isString(),
age: enforce.isNumber(),
tags: enforce.isArrayOf(enforce.isString()),
});
const suite = create(data => {
// data is typed as: { username: string; age: number; tags: string[] }
test('username', () => {
enforce(data.username).isNotBlank();
});
}, userSchema);
suite.run({ username: 'alice', age: 30, tags: [] }); // ✅
// suite.run({ username: 'alice' }); // ❌ Property 'age' is missing
// suite.run({ username: 42, age: 30, tags: [] }); // ❌ Type mismatch
// Field names are inferred from schema keys
suite.remove('username'); // ✅
suite.resetField('age'); // ✅
suite.focus({ only: 'username' }); // ✅
suite.only('tags'); // ✅
suite.afterField('username', () => {}); // ✅
// suite.remove('email'); // ❌ compile-time error
// suite.focus({ only: 'email' }); // ❌ compile-time error
const result = suite.get();
result.hasErrors('username'); // ✅
// result.hasErrors('email'); // ❌ compile-time error
```
Works with `enforce.shape`, `enforce.loose`, and `enforce.partial`.
### Try it
## Config Generic
Use `create` when you want to declare field and group names explicitly without a schema. This gives you full type safety over all suite APIs.
```typescript
import { create, test, enforce } from 'vest';
const suite = create<{
fields: 'username' | 'email' | 'password';
groups: 'auth' | 'profile';
}>(data => {
test('username', () => {}); // ✅
test('email', () => {}); // ✅
// test('phone', () => {}); // ❌ compile-time error
});
// All suite-level APIs are typed
suite.remove('username'); // ✅
suite.resetField('email'); // ✅
suite.focus({ only: 'password', onlyGroup: 'auth' }); // ✅
suite.only('username'); // ✅
suite.afterField('email', () => {}); // ✅
// suite.remove('phone'); // ❌ compile-time error
// suite.focus({ onlyGroup: 'billing' }); // ❌ compile-time error
const result = suite.get();
result.getErrors('username'); // ✅
// result.getErrors('phone'); // ❌ compile-time error
```
The `SuiteConfig` type is exported from Vest for reuse:
```typescript
import type { SuiteConfig } from 'vest';
```
### Try it
## Escape Hatch
Use `create()` when you need to opt out of field typing entirely. All APIs accept any string, which is useful for dynamic field generation or migration scenarios.
```typescript
import { create, test } from 'vest';
const suite = create((data: any) => {
test('dynamic_field', () => {});
});
// All APIs accept any string
suite.remove('anything');
suite.focus({ only: 'anything', onlyGroup: 'any_group' });
suite.only('anything');
suite.get().hasErrors('anything');
```
The escape hatch also works with a schema for runtime validation while keeping field names open:
```typescript
const suite = create((data: any) => {
test('anything', () => {});
}, schema);
```
## Untyped Fallback
When no generic is provided and no schema is passed, the suite accepts any string for field and group names. This is the default behavior and matches pre-typed Vest usage.
```typescript
import { create, test } from 'vest';
const suite = create((data: any) => {
test('whatever', () => {});
});
suite.remove('whatever'); // any string accepted
suite.focus({ only: 'whatever' }); // any string accepted
```
## Typed Suite-Level APIs
When a suite is typed (via schema or config generic), the following APIs all enforce field and group names at compile time:
| API | Accepts |
| --------------------------------------------------- | --------------------- |
| `suite.test(fieldName, ...)` | Field names |
| `suite.only(field)` | Field names |
| `suite.skip(field)` | Field names |
| `suite.include(field)` | Field names |
| `suite.optional(field)` | Field names |
| `suite.group(groupName, cb)` | Group names |
| `suite.remove(field)` | Field names |
| `suite.resetField(field)` | Field names |
| `suite.afterField(field, cb)` | Field names |
| `suite.focus({ only, skip, onlyGroup, skipGroup })` | Field and group names |
| `result.hasErrors(field)` | Field names |
| `result.getErrors(field)` | Field names |
| `result.hasWarnings(field)` | Field names |
| `result.getWarnings(field)` | Field names |
| `result.isValid(field)` | Field names |
| `result.isTested(field)` | Field names |
You can also destructure typed helpers from the suite:
```typescript
const { test, group, only, skip, include, optional } = suite;
// These inherit the suite's field/group types
```
## Suite Result Types
Use exported types to annotate variables and APIs:
- `Suite` - a suite instance.
- `SuiteResult` - the result returned from `run`, `runStatic`, or `get`.
- `SuiteSummary` - the static snapshot of all test results.
`SuiteResult` also carries `types.input` and `types.output` when a schema is present. When the schema uses parser chains (e.g., `isNumeric().toNumber()`), the input and output types are distinct:
```typescript
const schema = enforce.shape({
age: enforce.isNumeric().toNumber(),
name: enforce.isString().trim(),
});
const suite = create(data => {
test('age', () => {
enforce(data.age).greaterThan(0);
});
}, schema);
// suite.run() accepts the input type: { age: string | number; name: string }
const result = suite.run({ age: '25', name: ' alice ' });
// result.value is typed as the output type: { age: number; name: string }
result.value; // { age: 25, name: 'alice' }
```
`result.value` is typed as the schema output and is only available when the suite is valid (`result.valid === true`).
## Custom Enforce Rules
Extend `enforce` with value-first signatures so TypeScript can map them into both eager and lazy APIs.
```typescript
import { enforce } from 'vest';
const customRules = {
isValidEmail: (value: string) => value.includes('@'),
isWithinRange: (value: number, min: number, max: number) =>
value >= min && value <= max,
};
enforce.extend(customRules);
declare global {
namespace n4s {
interface EnforceMatchers {
isValidEmail: (value: string) => boolean;
isWithinRange: (value: number, min: number, max: number) => boolean;
}
}
}
enforce('test@example.com').isValidEmail();
enforce(10).isWithinRange(5, 15);
```
[Read more about custom rule typing](https://vestjs.dev/docs/enforce/creating_custom_rules#typescript-support).
# Understanding Vest's State
Vest suites keep validation state between runs. A focused run updates the fields it checked and leaves the other field results in place.
## Why Stateful Validation?
Imagine a form with 10 fields. When a user updates just the "username" field, you have two options:
1. **Re-validate everything** - Slow, and might flash errors on untouched fields
2. **Validate only "username"** - Fast, but you lose the state of other fields
With Vest, you can validate one field and keep the rest of the result.
### How It Works
```
Step 1: User fills "Password" field
└─→ suite.run() validates password
└─→ Result: { password: ✓ }
Step 2: User fills "Username" field
└─→ suite.only('username').run()
└─→ Vest runs ONLY username tests
└─→ Vest MERGES with previous password result
└─→ Result: { username: ?, password: ✓ } ← Full picture!
Step 3: User fixes username error
└─→ suite.only('username').run()
└─→ Result: { username: ✓, password: ✓ } ← Ready to submit!
```
This is why `isValid()` always gives you the complete answer - even when you only validated one field.
## What Vest's State Does
- **Skipped field merge**: When you focus on specific fields, Vest keeps the results of untouched fields.
- **Lagging async test blocking**: If an old async test finishes after a new one started, Vest ignores the stale result.
## When State Becomes a Problem
Stateful validation is great for forms, but sometimes you need to reset it.
### Problem 1: Navigation in SPAs
If a user submits a form successfully, navigates away, then comes back - the form still shows "success" state from the previous submission.
**Solution: Reset on mount**
```javascript
useEffect(() => {
suite.reset();
}, []);
```
### Problem 2: Dynamic Fields
If you dynamically add/remove fields (like items in a cart), removed fields still exist in Vest's state and might cause `isValid()` to return `false`.
**Solution: Remove the field**
```javascript
function handleRemoveItem(id) {
removeFromCart(id);
suite.remove(`item_${id}`);
}
```
## State Management Methods
### `suite.reset()`
Wipes all validation state. Use when:
- User clicks "Clear form"
- Component unmounts
- Starting a new transaction
```javascript
suite.reset();
```
### `suite.resetField(fieldName)`
Clears just one field's results. Use when:
- User clicks "clear" on an input
- You want to remove errors without re-validating
```javascript
suite.resetField('email');
```
### `suite.remove(fieldName)`
Removes a field from state entirely. Use when:
- A field is dynamically removed from the UI
- You want `isValid()` to stop considering that field
```javascript
suite.remove('couponCode');
```
## Stateless Alternative: `runStatic()`
If you're on the server or don't need state merging, use `runStatic()`:
```javascript
// Server-side: fresh validation every request
app.post('/register', (req, res) => {
const result = suite.runStatic(req.body);
// State is immediately discarded
});
```
## Summary
| Scenario | Solution |
| ----------------------- | --------------------------- |
| Form reset / navigation | `suite.reset()` |
| Clear single field | `suite.resetField('field')` |
| Remove dynamic field | `suite.remove('field')` |
| Server-side validation | `suite.runStatic()` |
Vest's statefulness is a feature, not a limitation. It's what makes incremental validation fast and accurate. When you need to escape it, the tools above give you full control.
# Upgrading from V5 to V6
Vest brings significant improvements to the API, focusing on better developer experience, type safety, and standard compliance.
## `create` returns a Suite Object
In V5, `create` returned a function that you would call directly to run the suite. In V6, `create` returns a **Suite Object** with methods like `.run()`, `.reset()`, and `.get()`.
```diff
- const suite = create(() => { ... });
- const result = suite(data);
+ const suite = create(() => { ... });
+ const result = suite.run(data);
```
## `suite.run()` returns a Promise-like Result
In V5, `suite()` returned the result object synchronously, and you had to use `promisify` or callbacks for async results. In V6, `suite.run()` returns a result object that is also a Promise.
```diff
- import { promisify } from 'vest';
- const runAsync = promisify(suite);
- const result = await runAsync(data);
+ const result = await suite.run(data);
```
## Removed `promisify` and `staticSuite`
These utilities have been removed in favor of the new Suite Object API.
- **promisify**: Use `await suite.run()` instead.
- **staticSuite**: Use `suite.runStatic()` instead.
```diff
- import { staticSuite } from 'vest';
- const suite = staticSuite(() => { ... });
- suite(data);
+ import { create } from 'vest';
+ const suite = create(() => { ... });
+ suite.runStatic(data);
```
## `test.memo` is now a top-level `memo` export
The memoization API has been promoted to a top-level export and can now wrap any part of the suite, not just single tests.
```diff
- import { create, test } from 'vest';
+ import { create, test } from 'vest';
+ import { memo } from 'vest/memo';
create(data => {
- test.memo('field', 'msg', () => { ... }, [data.field]);
+ memo(() => {
+ test('field', 'msg', () => { ... });
+ }, [data.field]);
});
```
## `done()` callback removed from Result
The `.done()` method on the result object has been removed. Use `suite.afterEach()` or `await suite.run()` instead.
```diff
- suite(data).done(result => { ... });
+ suite.afterEach(() => { ... }).run(data);
// OR
+ const result = await suite.run(data);
```
## Field-Focused Validation with `suite.focus()` and `suite.only()`
V6 introduces `suite.focus()` and `suite.only()` as the recommended way to run validation for specific fields. In V5, you had to pass the field name into the suite callback and call `only()` inside it. In V6, you declare what to focus on externally, keeping focus logic separate from validation logic.
```diff
- const suite = create((data, fieldName) => {
- only(fieldName);
- test('username', 'Username is required', () => { ... });
- test('email', 'Email is required', () => { ... });
- });
- suite(formData, 'email');
+ const suite = create(data => {
+ test('username', 'Username is required', () => { ... });
+ test('email', 'Email is required', () => { ... });
+ });
+ suite.only('email').run(formData);
```
`suite.focus()` accepts a config object with `only`, `skip`, `onlyGroup`, and `skipGroup` modifiers, allowing you to combine multiple criteria in a single call:
```javascript
suite.focus({ only: 'email', skipGroup: 'signUp' }).run(formData);
```
`suite.only(field)` is a shorthand for `suite.focus({ only: field })`.
The V5 pattern of calling `only()` and `skip()` inside the callback still works, but the new suite-level methods are recommended for cleaner separation of concerns.
## Standard Schema Support
Vest implements the [Standard Schema](https://github.com/standard-schema/standard-schema) spec. You can now use Vest suites directly with libraries that support this standard.
```javascript
const result = await suite['~standard'].validate(data);
```
Compatible libraries normally invoke this hook for you. Continue to use `suite.run(data)` for stateful Vest execution and `suite.runStatic(data)` for independent server execution.
---
## Automated Migration Prompt
You can use the following prompt with an LLM (like ChatGPT or Claude) to help migrate your codebase from Vest 5 to Vest 6.
```markdown
I am migrating my Vest validation suites from version 5 to version 6. Please refactor the following code according to these rules:
1. **Suite Creation**: `create` now returns a Suite Object, not a function.
- Change `const suite = create(...)` to keep the same variable name.
- Remove any suite name passed as the first argument to `create`.
2. **Running Suites**:
- Change `suite(data)` to `suite.run(data)`.
- Change `staticSuite(...)` to `create(...)` and run it with `suite.runStatic(data)`.
3. **Async Handling**:
- Remove `import { promisify } from 'vest'`.
- Remove `promisify(suite)`.
- Change `await suite(data)` or `promisified(data)` to `await suite.run(data)`.
- Remove `.done()` callbacks. Use `await suite.afterField('fieldName', callback)` or `suite.afterEach(callback)`.
4. **Memoization**:
- Change `test.memo(...)` to `memo(() => { test(...) }, deps)`.
- Ensure `memo` is imported from 'vest/memo': `import { memo } from 'vest/memo';`.
5. **Field-Focused Validation**:
- If the suite callback accepts a second argument used with `only(fieldName)` inside the callback, refactor it to use `suite.only(fieldName).run(data)` or `suite.focus({ only: fieldName }).run(data)` at the call site instead.
- Remove the extra callback parameter and the `only()` / `skip()` call from inside the callback body.
- For group-level skipping, replace `skip(true)` inside `group()` with `suite.focus({ skipGroup: 'groupName' }).run(data)`.
6. **General**:
- Keep all validation logic intact.
- Preserve comments.
```
---
# Upgrading from V4 to V5
### Migration guide
Vest 5 is mostly compatible with Vest 4, but some changes were made. In most cases, if you do not change anything, vest will keep working as it did before. However, to take advantage of the new features, you'll need to make some changes.
## Eager execution mode is now the default
In previous versions of Vest, Vest continued validating fields even after one of their tests had failed. V5 changes that to improve the runtime performance, and instead, Vest will halt further validations of a given field if it failed. This was an opt-in feature, and it can now be removed. [Read more on execution modes](https://vestjs.dev/docs/writing_your_suite/execution_modes).
```diff
- import {create, test, eager} from 'vest';
+ import {create, test} from 'vest';
const suite = create(() => {
- eager();
test(/*...*/);
});
```
To bring back the previous behavior, use the `mode` function that alters the execution mode:
```diff
- import {create, test} from 'vest';
+ import {create, test, mode, Modes} from 'vest';
const suite = create(() => {
+ mode(Modes.ALL);
test(/*...*/);
});
```
This also means that if you've used `skipWhen` to avoid running of failing fields, you can now remove it:
```diff
- import {create, test, skipWhen} from 'vest';
+ import {create, test} from 'vest';
const suite = create(() => {
- skipWhen(res => res.hasErrors('username'), () => {
test('username', 'username already taken', () => {
// ...
});
- });
});
```
## All result methods are now available directly on the result object
In previous versions, you had to call `suite.get()` to access the different methods, such as `getErrors` and `isValid`. In V5, these methods are available directly on the result object returned from `suite.run()`. [Read more](https://vestjs.dev/docs/writing_your_suite/accessing_the_result).
```diff
- suite.get().getErrors('username');
+ result.getErrors('username');
- suite.get().isValid();
+ result.isValid();
```
## Added `hasError` and `hasWarning` methods
The result object has two new methods: hasError and hasWarning. They return a boolean value indicating whether a given field has an error or a warning. With these new methods, you can display the first error of a field. [Read more](https://vestjs.dev/docs/writing_your_suite/accessing_the_result).
```diff
- res.getError('username')
+ res.hasError('username')
```
## Removed skip.group and only.group
Vest 5 removes the dedicated group interface for skip and only, and instead allows you to call skip and only directly within the groups. [Read more](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/skip_and_only).
```diff
const suite = create(() => {
- skip.group('group1', 'username');
group('group1', () => {
+ skip('username');
test('username', 'message', () => {
// ...
});
});
});
```
```diff
const suite = create(() => {
- skip.group('group1');
group('group1', () => {
+ skip(true);
test('field1', 'message', () => {
// ...
});
});
});
```
## Optional fields now take into account the suite params
In previous versions, optional fields only took into consideration whether the tests ran or not. In V5 optional fields also search the data object passed to the suite. If it has an object with the optional field in it, and the optional field is blank - the test will be considered valid even if it is not passing.
[Read more on optional fields](https://vestjs.dev/docs/writing_your_suite/optional_fields).
## Server side validations are built-in
In previous versions, as a user of Vest you had to set up your own state-reset mechanism. Vest now has a `staticSuite` export that does that for you. [Read more on Server Side Validations](https://vestjs.dev/docs/server_side_validations).
```diff
- import {create} from 'vest';
+ import {staticSuite} from 'vest';
- const suite = create(() => {/*...*/});
+ const suite = staticSuite(() => /*...*/});
- function ServerValidation() {
- suite.reset();
- suite.run();
- }
```
## First-Class-Citizen typescript support
All of Vest's methods are now typed and make use of generics to enforce correct usage throughout your suite. [Read More on TypeScript support](https://vestjs.dev/docs/typescript_support).
## Dropped support for \
```jsx
import { create, test, enforce } from 'vest';
import 'vest/email';
import { useState } from 'react';
const suite = create((data = {}) => {
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', 'Please enter a valid email', () => {
enforce(data.email).isEmail();
});
});
function SignupForm() {
const [formData, setFormData] = useState({ username: '', email: '' });
const handleChange = (name, value) => {
const nextState = { ...formData, [name]: value };
suite
.only(name)
.afterEach(() => setFormData(nextState))
.run(nextState);
};
const handleSubmit = async e => {
e.preventDefault();
await suite.run(formData);
if (suite.isValid()) {
// Submit form
console.log('Form is valid!', formData);
}
};
return (
);
}
```
## Custom Hook Pattern
Create a reusable hook for form validation:
```jsx
import { useCallback, useState } from 'react';
function useVestForm(suite, initialData = {}) {
const [formData, setFormData] = useState(initialData);
const validate = useCallback(
(fieldName, value) => {
const newData = { ...formData, [fieldName]: value };
suite
.only(fieldName)
.afterEach(() => {
setFormData(newData);
})
.run(newData);
},
[formData, suite],
);
const validateAll = useCallback(() => {
suite
.afterEach(() => {
// Create a new object reference to trigger a re-render
setFormData(current => ({ ...current }));
})
.run(formData);
}, [formData, suite]);
return {
formData,
validate,
validateAll,
setFormData,
};
}
// Usage
function MyForm() {
const { formData, validate, validateAll } = useVestForm(suite);
const handleSubmit = e => {
e.preventDefault();
validateAll();
if (!suite.hasErrors()) {
// Submit form
}
};
return ;
}
```
## Async Validation
Handle async validations like API calls:
```jsx
import { create, test, enforce } from 'vest';
import 'vest/email';
const suite = create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
test('username', 'Username is already taken', async () => {
// This test will run asynchronously
await enforce(data.username).isNotBlank();
const response = await fetch(
`/api/check-username?username=${data.username}`,
);
const { available } = await response.json();
enforce(available).isTruthy();
});
});
function UsernameField() {
const [username, setUsername] = useState('');
const [isChecking, setIsChecking] = useState(false);
const handleChange = value => {
setIsChecking(true);
suite
.only('username')
.afterEach(() => {
setUsername(value);
setIsChecking(false);
})
.run({ username: value });
};
return (
);
}
```
## Best Practices
### 1. Create Suite Outside Component
Define your validation suite in a separate file or outside the component. Vest suites are stateful, so they should be treated as singletons for a given form.
```javascript
// validations/signupSuite.js
import { create, test, enforce } from 'vest';
export const suite = create(data => {
// validations
});
```
```jsx
// components/SignupForm.jsx
import { suite } from '../validations/signupSuite';
import { useState } from 'react';
function SignupForm() {
// ...
}
```
### 2. Field-Level Validation
Validate individual fields on change for better UX:
```jsx
const handleFieldChange = (fieldName, value) => {
const nextState = { ...formData, [fieldName]: value };
// Only validate the changed field
suite
.only(fieldName)
.afterEach(() => setFormData(nextState))
.run(nextState);
};
```
### 3. Form-Level Validation on Submit
Validate all fields before submission:
```jsx
const handleSubmit = e => {
e.preventDefault();
// Validate all fields
suite
.afterEach(() => {
if (!suite.hasErrors()) {
// Submit form
submitForm(formData);
}
})
.run(formData);
};
```
## TypeScript Support
Vest has [excellent TypeScript support](https://vestjs.dev/docs/typescript_support):
```tsx
import { create, test, enforce } from 'vest';
import 'vest/email';
interface FormData {
username: string;
email: string;
age: number;
}
const suite = create((data: Partial = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
test('email', 'Email is required', () => {
enforce(data.email).isNotBlank();
});
test('age', 'Must be 18 or older', () => {
enforce(data.age).greaterThanOrEquals(18);
});
});
function TypedForm() {
const [formData, setFormData] = useState>({});
// Rest of component
}
```
## Next Steps
- Explore [Core Concepts](https://vestjs.dev/docs/concepts) to understand Vest's architecture
- Learn about [The Test Function](https://vestjs.dev/docs/writing_tests/the_test_function) for advanced validation patterns
- Check out [Async Tests](https://vestjs.dev/docs/writing_tests/async_tests) for handling asynchronous validations
# Using Vest with Vue
Vest integrates beautifully with Vue 3's Composition API and reactivity system, providing declarative validation for your forms.
## Quick Start with Composition API
import VueIntegration from '@site/src/components/Sandpack/VueIntegration';
```vue
```
## Composable Pattern
Create a reusable composable for form validation:
```js
// composables/useVestForm.js
import { ref, reactive, toRefs } from 'vue';
export function useVestForm(suite, initialData = {}) {
const formData = reactive({ ...initialData });
const result = ref(suite.get());
const isValidating = ref(false);
const validate = fieldName => {
isValidating.value = true;
suite
.only(fieldName)
.afterEach(() => {
result.value = suite.get();
isValidating.value = false;
})
.run(formData);
};
const validateAll = () => {
isValidating.value = true;
suite
.afterEach(() => {
result.value = suite.get();
isValidating.value = false;
})
.run(formData);
};
const reset = () => {
Object.keys(formData).forEach(key => {
formData[key] = initialData[key] || '';
});
suite.reset();
result.value = suite.get();
};
return {
formData,
result,
isValidating,
validate,
validateAll,
reset,
};
}
```
Usage:
```vue
```
## Async Validation
Handle async validations with Vue's reactivity:
```vue
Checking availability...
{{ res.getError('username') }}
Username is available!
```
## Options API Pattern
For Vue 2 or Options API users:
```vue
```
## TypeScript Support
Vest works great with [TypeScript](https://vestjs.dev/docs/typescript_support) in Vue:
```vue
```
## Best Practices
### 1. Create Suite Outside Component
Define your validation suite in a separate file. Vest suites are stateful, so they should be treated as singletons for a given form.
```js
// validations/signupSuite.js
import { create, test, enforce } from 'vest';
import 'vest/email';
export const signupSuite = create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
// ... more tests
});
```
```vue
```
### 2. Debounce Async Validations
Use Vue's `watchDebounced` or a debounce utility for expensive validations:
```vue
```
### 3. Show Errors After Touch
Only show errors after a field has been touched:
```vue
{{ res.getError('username') }}
```
## Next Steps
- Learn about [Core Concepts](https://vestjs.dev/docs/concepts) to understand Vest's architecture
- Explore [Async Tests](https://vestjs.dev/docs/writing_tests/async_tests) for handling asynchronous validations
- Check out [Skip and Only](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/skip_and_only) for complex validation scenarios
# classnames
After validating user input, you usually need to also indicate the validation result on the page - most of the times by adding a class to your input element. One of the difficulties you are likely to face is that the logic for setting the class is not always the negation of `hasErrors`.
```js
const addIsValidClass = !res.hasErrors('fieldName'); // this does not ALWAYS mean 'valid'
```
What about when the field is skipped or not validated yet? It does not have errors, so `res.hasErrors('fieldName')` will return `false`, and by that logic, you might mistakenly add a `is-valid` class to your element.
In this case you will also need to check if the test actually ran - so:
```js
const addIsValidClass = res.tests[fieldName] && !res.hasErrors('fieldName');
```
But this can get pretty cumbersome when added to multiple fields with different criteria (untested, invalid, hasWarning...).
This is what `vest/classnames` is for. It is a tiny utility function, that allows you to specify classnames to be added for each criteria.
The way it works is simple. You call `classnames` with your result object, and the list of classes you want to be added for whenever the field is tested, untested, has warning or is invalid. It then returns a function that when called with a field name, returns a space delimited string of classes. If more than one class applies (both tested and invalid, for example) they will both be added to the string.
```js
import classnames from 'vest/classnames';
import suite from './suite';
const res = suite.run(data);
const cn = classnames(res, {
untested: 'is-untested', // will only be applied if the provided field did not run yet
tested: 'some-tested-class', // will only be applied if the provided field did run
invalid: 'my_invalid_class', // will only be applied if the provided field ran at least once and has an error
valid: 'my_valid_class', // will only be applied if the provided field ran at least once does not have errors or warnings
warning: 'my_warning_class', // will only be applied if the provided field ran at least once and has a warning
pending: 'my_pending_class', // will only be applied if the provided field has pending async tests
});
const fieldOneClasses = cn('field_1'); // "is-untested"
const fieldTwoClasses = cn('field_2'); // "some-tested-class my_invalid_class"
const fieldThreeClasses = cn('field_3'); // "some-tested-class my_warning_class"
```
# Vest, Schema Validators, and Form Libraries
Vest can work alongside the form and schema tools you already use.
These capabilities can be composed, but they are not mutually exclusive:
| Tool or layer | What it can own |
| -------------------------------- | -------------------------------------------------- |
| Form managers such as RHF/Formik | Values, registration, events, and submission |
| Enforce schemas, Zod, or Valibot | Structural validation, transformation, and parsing |
| **Vest suites** | Stateful and stateless validation workflows |
| **Vest suite + Enforce schema** | Both parsed boundaries and validation over time |
## Where Vest differs
A schema validator is excellent at answering:
> Does this submitted payload have the expected structure and values?
A Vest suite also needs to answer:
> The user changed one part of a workflow. What needs to run now, what previous state remains valid, and which async result should be trusted?
Vest can handle both jobs. Enforce schemas validate, transform, and parse data. A Vest suite keeps test results up to date while the user moves through a form.
## Vest and Zod
Zod is a strong choice for:
- parsing unknown input;
- validating API and persistence boundaries;
- defining data structures;
- deriving TypeScript types;
- one-shot validation.
Enforce supports those same core boundary jobs. A schema created with `enforce.shape`, `enforce.loose`, or `enforce.partial` exposes `.parse()` and returns transformed output:
```ts
const accountSchema = enforce.shape({
age: enforce.isNumeric().toNumber(),
email: enforce.isString().trim(),
});
const account = accountSchema.parse(input);
// account.age is a number
```
Attach the schema to a suite when the same boundary and parsed values should participate in Vest's runtime:
```ts
const accountSuite = create(parsedAccount => {
test('age', 'Must be an adult', () => {
enforce(parsedAccount.age).greaterThanOrEquals(18);
});
}, accountSchema);
const result = accountSuite.runStatic(input);
if (result.isValid()) persist(result.value);
```
Vest is a strong choice for:
- validating one field or step at a time;
- retaining earlier validation results;
- coordinating overlapping async checks;
- dependent and conditional fields;
- multi-step workflows;
- warnings, pending state, and progressive completion.
### Use Vest on its own or with Zod
Vest can own both the interactive workflow and the submitted boundary:
```text
User interaction
→ Vest runs focused, stateful validation
→ UI shows errors, warnings, and pending work
Final submission
→ Enforce schema parses and transforms the complete payload
→ Vest runStatic applies the business rules
```
If your application already uses Zod, parse the payload before a stateless Vest run:
```ts
const parsed = schema.parse(payload);
const result = await suite.runStatic(parsed);
```
Zod and Enforce can both parse the boundary. The Vest suite is what carries the validation result from one interaction to the next.
## Vest and form state managers
Vest does not register inputs, store their values, or submit the form. That makes it possible to use Vest:
- directly with local state;
- with React Hook Form or Formik;
- from Vue or Svelte composables;
- in a custom workflow engine;
- on the server without a UI.
A form manager and Vest can therefore work together: the form manager owns input mechanics while Vest owns progressive validation behavior.
Vest implements Standard Schema so compatible tools can consume suites and Enforce rules through a shared validation interface. Application code should use `suite.run()` for stateful validation or `suite.runStatic()` for independent server validation. The Standard Schema `~standard.validate` hook exists for compatible consumers, not as Vest's general execution API.
## Capability comparison
| Capability | Standalone schema tool | Form state manager | Vest |
| ----------------------------------- | ---------------------- | ------------------ | ---------------------------------- |
| Parse a complete object boundary | Primary | Varies | `enforce.shape(...).parse()` |
| Own field values and registration | No | Primary | No |
| Run selected fields or groups | Varies | Often | Primary |
| Retain validation between runs | No | Often | Primary |
| Prevent stale async results | Usually manual | Varies | Built in |
| Model dependent fields | Cross-object rule | Varies | `include` and ordinary JS |
| Model conditional sections | Unions/refinements | Varies | `skipWhen`, `omitWhen`, `optional` |
| Warnings that do not block submit | Usually custom | Varies | Built in |
| Share rules across UI frameworks | Yes | Usually no | Yes |
| Stateless server execution | Yes | Not primary | `runStatic()` |
| Resume full server validation state | No | Framework-specific | `SuiteSerializer` |
## Why use Vest instead of writing validation state manually?
It is possible to assemble focused validation, pending flags, request IDs, abort controllers, field dependencies, caches, and error merging by hand.
Vest packages those behaviors into one validation runtime with a consistent result model. This removes plumbing that otherwise spreads across components and becomes difficult to test as the workflow grows.
## When Vest is the right tool
Vest becomes more valuable as a workflow gains:
- asynchronous checks;
- dependencies between fields;
- multiple steps or conditional sections;
- dynamic lists;
- validation shared across browser and server;
- expensive checks that should not rerun unnecessarily;
- UX distinctions between untested, pending, warning, error, and valid.
## When another tool may be enough
Prefer a simpler solution when:
- native HTML validation covers the requirement;
- the form is trivial and entirely synchronous;
- validation happens only once at an API boundary;
- your primary problem is storing form values rather than validation behavior.
You do not have to replace your schema or form library to use Vest. Add it when keeping validation correct between interactions is the hard part.
# Debouncing Tests
## `debounce()`
The `debounce()` function in Vest helps you optimize function execution by introducing a delay. This is useful in scenarios where a function is called repeatedly due to user interaction, and you only want to execute the latest version after a period of inactivity.
### Usage
**1. Import Debounce**
```js
import debounce from 'vest/debounce';
```
**2. Wrap your Test Callback:**
```js
test(
'username',
'User already taken',
debounce(async () => {
await doesUserExist();
}, 2000),
);
```
In the above example, Vest will wait for two seconds before executing the test, and it will be run only once, no matter how many times the suite was invoked during this time period.
:::caution IMPORTANT
When using `debounce`, all debounced tests are treated as async, even if the test callback is synchronous. This is because the test will be executed after the debounce period, which is an async operation.
:::
# Dynamic Tests with `each`
Forms often have dynamic lists: a list of travelers, a set of product attributes, or multiple shipping addresses. You don't know how many fields you'll have until runtime.
Vest provides the `each` helper to handle these lists efficiently.
## Using `each`
Think of `each` as a smarter `forEach` loop designed specifically for validation. It takes two arguments:
1. The array to iterate over.
2. A callback function to run for every item.
The key for tracking goes on each `test` call as the last argument.
```javascript
import { create, test, each, enforce } from 'vest';
const suite = create(data => {
// data.products is an array of objects
each(data.products, product => {
test(
'product_name',
'Name is required',
() => {
enforce(product.name).isNotBlank();
},
product.id,
); // <--- The key goes on the test
test(
'product_price',
'Price must be positive',
() => {
enforce(product.price).greaterThan(0);
},
product.id,
);
});
});
```
## Why do I need a Key?
You know how React warns you if you render a list without a `key` prop? Vest needs keys for the same reason.
Vest is stateful. It remembers that the test for "Product A" failed. If you reorder the list or delete "Product A", Vest needs to know that the tests associated with "Product A" should move or disappear, rather than attaching the old error to the new item in that index.
### How to specify the Key
The key is the last argument to `test`. It can be any string or number that uniquely identifies the item:
```javascript
each(data.items, item => {
test(
'item_name',
'Name required',
() => {
enforce(item.name).isNotBlank();
},
item.id,
); // Uses item.id as the key
});
```
You can also use a computed key:
```javascript
each(data.items, item => {
test(
'item_name',
'Name required',
() => {
enforce(item.name).isNotBlank();
},
`${item.type}_${item.id}`,
);
});
```
:::danger Avoid using Index
Never use the array index as a key (or omit the key argument, which defaults to index). If the user deletes the first item, the second item will inherit the first item's validation state (and errors!), leading to a confusing UI.
:::
# Grouping tests
In some cases it can be helpful to group tests together so you can include or exclude a portion of the suite with a single condition.
Similar to the `describe` and `context` features provided by unit testing frameworks, Vest provides `group()`.
## Usage
There are two ways to use `group()`:
- **Named Groups** `group(name, callback)` - creates a new group with the given name and runs the tests inside the callback. Named groups are added to the result object, and can be queried for errors and warnings.
- **Unnamed Groups** `group(callback)` - Runs the tests inside the callback without creating a new group. This is useful for skipping tests, without changing the result object.
## Named Groups
```js
import { create, test, group, enforce } from 'vest';
create(data => {
group('group_name', () => {
test('field_name', 'error_message', () => {
enforce(data.field_name).equals('value');
});
});
});
```
## Unnamed Groups
```js
import { create, test, group, enforce } from 'vest';
create(data => {
group(() => {
test('field_name', 'error_message', () => {
enforce(data.field_name).equals('value');
});
});
});
```
# Full Example
```js
import { create, test, group, enforce, skip } from 'vest';
create(data => {
test('userName', "Can't be empty", () => {
enforce(data.username).isNotEmpty();
});
test('password', "Can't be empty", () => {
enforce(data.password).isNotEmpty();
});
group('signIn', () => {
skip(!data.userExists); // Skips the signin group if userExists is false
test(
'userName',
'User not found. Please check if you typed it correctly.',
findUserName(data.username),
);
});
group('signUp', () => {
skip(!!data.userExists); // Skips the signup group if userExists is true
test('email', 'Email already registered', isEmailRegistered(data.email));
test('age', 'You must be at least 18 years old to join', () => {
enforce(data.age).largerThanOrEquals(18);
});
});
});
```
## Use cases
### 1. Skipping groups with `suite.focus()`
The simplest way to skip groups is using `suite.focus({ skipGroup })` from outside the suite. This keeps your suite definition clean and moves the skip logic to the call site:
```js
import { create, test, group, enforce } from 'vest';
const suite = create(data => {
group('overview_tab', () => {
test('productTitle', 'Must be at least 5 chars.', () => {
enforce(data.productTitle).longerThanOrEquals(5);
});
test('productDescription', "Can't be longer than 2500 chars.", () => {
enforce(data.productDescription).shorterThanOrEquals(2500);
});
});
group('pricing_tab', () => {
test('price', '5$ or more.', () => {
enforce(data.price).gte(5);
});
test('productExtras', "Can't be empty.", () => {
enforce(data.extras).isNotEmpty();
});
});
});
```
```js
// Only validate the overview tab - skip pricing
suite.focus({ skipGroup: 'pricing_tab' }).run(data);
// Only validate the pricing tab - skip overview
suite.focus({ skipGroup: 'overview_tab' }).run(data);
// Validate everything on submit (no focus)
suite.run(data);
```
`skipGroup` internally injects a `skip(true)` call at the start of each matching group's callback. Because it creates a transient isolate, it adds zero overhead to the suite state.
For more details, see [Focused Updates](https://vestjs.dev/docs/writing_your_suite/focused_updates).
### 2. Multi-stage form with `skip()` inside the suite
You may have in your application a multi-screen form in which you want to validate each screen individually but submit it all at once.
```js
// suite.js
import { create, test, group, enforce, only } from 'vest';
const suite = create((data, currentTab) => {
group('overview_tab', () => {
skip(currentTab !== 'overview_tab');
test('productTitle', 'Must be at least 5 chars.', () => {
enforce(data.productTitle).longerThanOrEquals(5);
});
test('productDescription', "Can't be longer than 2500 chars.", () => {
enforce(data.productDescription).shorterThanOrEquals(2500);
});
test('productTags', 'Please provide up to 5 tags', () => {
enforce(data.tags).lengthEquals(5);
});
});
group('pricing_tab', () => {
skip(currentTab !== 'pricing_tab');
test('price', '5$ or more.', () => {
enforce(data.price).lte(5);
});
test('productExtras', "Can't be empty.", () => {
enforce(data.extras).isNotEmpty();
});
});
});
export default suite;
```
```js
// myFeature.js
suite.run(data, 'overview_tab'); // will only validate 'overview_tab' group
suite.run(data, 'pricing_tab'); // will only validate 'pricing_tab' group
```
### 3. Skipping only some of the tests of a given field
If we want to conditionally skip a portion of our suite, we can use `skip()` within a group.
```js
import { create, test, group, enforce, skip } from 'vest';
const suite = create(data => {
// We want to always run this test, even if we skip the promo_code quantity test
test('quantity', `Quantity on this item is limited to ${data.limit}`, () => {
enforce(data.quantity).lessThanOrEquals(data.limit);
});
group('promo_code', () => {
skip(!data.usedPromo); // Skips the group if usedPromo is false
test(
'quantity',
'promo code purchases are limited to one item only',
() => {
enforce(data.quantity).equals(1);
},
);
test(
'promoCode',
'Promo code can only be used once',
isPromoCodeUsed(data.usedPromo),
);
});
});
```
## Querying the result object for groups
Named Groups represent a portion of your validation suite, so when using `group`, you are likely to need to get the group-specific validation results.
Your result object exposes the following methods:
- `hasErrorsByGroup(groupName, /*optional:*/ fieldName)`
- `hasWarningsByGroup(groupName, /*optional:*/ fieldName)`
- `hasErrorsByGroup(groupName, /*optional:*/ fieldName)`
- `hasWarningsByGroup(groupName, /*optional:*/ fieldName)`
- `isValidByGroup(groupName, /*optional:*/ fieldName)`
Read more about these methods in [the result object](https://vestjs.dev/docs/writing_your_suite/accessing_the_result).
# Memoizing Tests
Vest introduces a new, top-level `memo` function. It allows you to cache parts of your suite execution based on dependencies, preventing unnecessary re-runs of expensive logic or async tests.
## Why Memoize?
Validation suites often contain expensive operations, such as:
- **Async checks**: Checking if a username is taken (network request).
- **Heavy computations**: Validating complex data structures.
If the relevant data hasn't changed (e.g., the user is typing in the "password" field, but the "username" field hasn't changed), re-running the username check is wasteful. Memoization lets you skip these tests and reuse the previous result.
Unlike the previous `test.memo` which was limited to single tests, the new `memo` can wrap **any** part of your suite - single tests, multiple tests, groups, or arbitrary logic.
## Usage
Import `memo` from `vest`:
```javascript
import { create, test } from 'vest';
import { memo } from 'vest/memo';
const suite = create(data => {
// cacheKey: [data.username]
// If data.username hasn't changed since the last run,
// this block is skipped, and previous results are restored.
memo(() => {
test('username', 'Username is taken', async () => {
await checkAvailability(data.username);
});
}, [data.username]);
});
```
## Memoizing Groups
You can use `memo` to skip entire groups of tests if their relevant data hasn't changed.
```javascript
memo(() => {
group('shipping_address', () => {
test('street', 'Required', () => {
/* ... */
});
test('city', 'Required', () => {
/* ... */
});
});
}, [data.shipping_address]);
```
## Advanced Configuration
`memo` accepts an optional third parameter: an options object to further control caching behavior.
```javascript
memo(
() => {
test('username', 'Username is taken', async () => {
await checkAvailability(data.username);
});
},
[data.username],
{
cacheSize: 5, // Keep up to 5 previous results
ttl: 10000, // Invalidate cache after 10 seconds
},
);
```
### Options
| Option | Type | Default | Description |
| :---------- | :------- | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
| `cacheSize` | `number` | `5` | The maximum number of previous memo results to retain in the cache. When the limit is reached, the oldest result is evicted. |
| `ttl` | `number` | `undefined` | Time-to-Live in milliseconds. If the cached result is older than the `ttl`, the cache will be invalidated and the test block will be re-run. |
## How it works
1. Vest checks the dependency array passed as the second argument.
2. If the dependencies match the previous run, the callback function is **not executed**.
3. Instead, Vest restores the test results (pass/fail/warn) produced by that block in the previous run.
# Async Tests
Vest supports asynchronous validation tests (e.g., checking if a username exists on the server).
import AsyncTestsSandpack from '@site/src/components/Sandpack/AsyncTests';
:::note
In Vest, `suite.run()` returns a hybrid result object that is also a Promise. This means you can `await` it directly.
:::
## Handling Race Conditions
One of Vest's superpowers is **built-in race condition handling**. Consider this scenario:
1. User types "A" → async validation starts
2. User types "AB" → another async validation starts
3. User types "ABC" → another async validation starts
4. Validation for "A" returns (slow network)
**Without Vest:** The old "A" result might overwrite the current "ABC" validation, causing incorrect UI state.
**With Vest:** Vest automatically discards stale results. Only the most recent validation for each field is processed.
```javascript
// User types quickly: "A" → "AB" → "ABC"
// Even if "A" validation finishes last, Vest ignores it
// and only uses the "ABC" result
suite.run({ username: 'ABC' });
```
This is handled automatically - no extra code required.
## Handling Async Results
Because async tests take time to complete, `suite.run()` creates a result object that is initially "pending".
### Option 1: Awaiting the Result
In Vest, `suite.run()` returns a Promise-like object if there are async tests. You can simply `await` it.
```javascript
const result = await suite.run(data);
if (result.isValid()) {
submitForm();
}
```
### Synchronous Access
`suite.run()` returns a hybrid object: it acts like a Promise for async completion, but its sync selectors are available immediately.
```javascript
const result = suite.run(data);
// Sync selectors are available right away
if (result.hasErrors('password')) {
showPasswordError();
}
if (result.isPending('username')) {
showSpinner();
}
// Await for final async completion
await result;
```
### Option 2: Using `.afterEach()`
If you prefer callbacks, or cannot use `await` at the call site, use the `.afterEach()` hook.
```javascript
suite
.afterEach(() => {
// This runs after the initial sync completion and after each async test finishes
const result = suite.get();
if (result.isValid()) {
submitForm();
}
})
.run(data);
```
:::info
Unlike `await`, the `afterEach` callback may run **multiple times** (once for sync completion, and again for each async completion). [Read more about Handling Suite Completion](https://vestjs.dev/docs/writing_your_suite/handling_completion).
:::
## Using AbortSignal
> Since 5.1.0
Each test function is passed an object with a `signal` property. This signal is an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) which can be used to terminate your async operations once a test is canceled.
### Why use AbortSignal?
When a user types quickly, Vest cancels the previous async test and starts a new one. The AbortSignal lets you:
1. **Stop unnecessary network requests** - Don't waste bandwidth on stale validations
2. **Cancel fetch requests** - Pass the signal to `fetch()` for automatic cancellation
3. **Clean up resources** - Check `signal.aborted` to bail early
```javascript
test('username', 'Already Taken', async ({ signal }) => {
// Early exit if already aborted
if (signal.aborted) return;
const response = await fetch('/check-username', {
signal, // Pass to fetch for automatic cancellation
method: 'POST',
body: JSON.stringify({ username: data.username }),
});
const { exists } = await response.json();
enforce(exists).isFalsy();
});
```
[More on AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal).
## Best Practices for Async Validation
### 1. Debounce at the UI layer
Don't call `suite.run()` on every keystroke. Debounce in your component:
```javascript
const debouncedValidate = debounce(data => {
suite.run(data);
}, 300);
```
### 2. Show pending state
Use `isPending()` to show loading indicators:
```javascript
const result = suite.run(data);
if (result.isPending('username')) {
showLoadingSpinner();
}
```
### 3. Combine with `suite.only()`
For the best UX, only run async tests for the field the user is interacting with. Use `suite.only(fieldName)` for the common case of targeting a single field:
```javascript
function handleBlur(fieldName) {
suite.only(fieldName).run(formData);
}
```
This skips expensive async checks for fields the user hasn't touched yet.
# Failing with a custom message
Custom messages can be very useful when you don't know the validation result in the client, but get it from the server instead. This can happen when you're performing validation on user input and sending it to a server for processing. If there's an issue with the input, the server will send back a validation result, but it may not be immediately clear what went wrong. This is where custom messages can help.
# Implementing Custom Messages in Vest
Vest allows you to provide custom messages within the test body itself. There are a few different ways to do this:
## Enforce message
The `enforce` function in Vest allows you to pass a custom message using the `message` modifier. This can be useful if you have multiple failure conditions.
The message must be specified before the rule it refers to, because once the rule fails, enforce throws immediately.
```js
test('username', () => {
enforce(data.username)
.message('Username is required')
.isNotBlank()
.message('Username must be at least 3 characters')
.longerThan(2);
});
```
## Throwing a string
If the message parameter is omitted, and the test throws a string value, the string will be used as the test's message. This can be useful if you want to provide a custom message for a specific validation rule.
```js
test('price', () => {
if (price < 0) {
throw 'Price must be positive';
}
});
```
```js
enforce.extend({
isPositive: value => {
return {
pass: value > 0,
message: () => 'value must be positive',
};
},
});
test('price', () => {
enforce(price).isPositive(); // will fail with the message: "value must be positive"
});
```
## Rejecting with a string
Async tests can reject their promise with the string as well:
```js
test('price', () => {
return apiCall().then(() => {
throw 'Price must be positive';
});
});
test('price', () => {
return Promise.reject('Price must be positive');
});
```
# The Test Function
The `test` function is the main function in Vest that holds your validation logic. It accepts the following arguments:
| Name | Type | Optional | Description |
| ---------- | ---------- | -------- | --------------------------------------------------------------------- |
| `name` | `String` | No | The name of the value or field that is validated. |
| `message` | `String` | Yes | An error message to display to the user in case of a failure. |
| `callback` | `Function` | No | The actual validation logic for the given test. |
| `key` | `String` | Yes | A unique key used to retain test value when reordering dynamic tests. |
A test can either be synchronous or asynchronous, and it can either have a severity of `error` or of `warn`.
## How to fail a test?
There are two ways to fail a test:
### Throwing an exception (using enforce)
Just like in most unit testing frameworks, a validation fails whenever the test body throws an exception.
```js
// const username = 'Gina.Vandervort';
// const password = 'Q3O';
test('username', 'Should be at least 3 characters long', () => {
enforce(username).longerThanOrEquals(3);
}); // this test passes
test('password', 'Should be at least 6 characters long', () => {
enforce(password).longerThanOrEquals(6); // an error is thrown here
}); // this test fails
```
```js
enforce.extend({
isChecked: value => {
return {
pass: !!value.checked,
message: () => 'value must be checked',
};
},
});
/*...*/
/*
tost = { checked: false }
*/
test('tos', () => {
enforce(tos).isChecked(); // will fail with the message: "value must be checked"
});
```
### Returning false
To make it easy to migrate your existing validation logic into Vest, it also supports validations explicitly returning `false` (and not any other falsy value) to represent failures.
```js
// const username = 'Gina.Vandervort';
// const password = 'Q3O';
test('username', 'Should be at least 3 characters long', () => {
return username.length >= 3; // = true
}); // this test passes
test('password', 'Should be at least 6 characters long', () => {
return password.length >= 6; // = false
}); // this test fails
```
### Rejecting a Promise (Asynchronous Tests)
Asynchronous tests are executed asynchronously and return a Promise. The Promise will be resolved with a boolean value indicating whether the test passed or failed, or rejected with an error.
To fail an asynchronous test, you can either throw an Error in the async function or reject the Promise.
```js
test('email', 'email is already taken', async () => {
const isTaken = await isEmailTaken(data.email);
enforce(isTaken).isFalse();
});
```
Or with a promise:
```js
test('email', 'email is already taken', () => {
return isEmailTaken(data.email);
});
// returns a promise that rejects if the email is taken
function isEmailTaken(email) {
/*...*/
}
```
[Read more](https://vestjs.dev/docs/writing_tests/async_tests) on async tests.
# Warn-only tests
By default, a failing test has a severity of `error`. Sometimes you may need to lower the severity level of a given test to `warn` so that even when it fails, it should not prevent submission. An example of this would be validating password strength.
To set a test's severity level to `warn`, call `warn()` from the body of your test.
If you need to set warning severity after an `await` in an async test, use `useWarn()` and call the setter it returns.
import WarnOnlyTestsSandpack from '@site/src/components/Sandpack/WarnOnlyTests';
## `warn()` vs `useWarn()` in async tests
- You may only use `warn()` and `useWarn()` from within the body of a `test` function.
- `warn()` should be called in the synchronous portion of your test.
- `useWarn()` is the async-safe alternative when warning severity needs to be set later.
```js
// ✔
test('password', async () => {
warn();
return await someAsyncFunction();
});
// ✔
test('password', () => {
warn();
return anAsyncFunction();
});
// 🚨 This will result in your warn() call not taking effect
test('password', async () => {
await someAsyncFunction();
warn(); // 🚨
});
// 🚨 This will result in your warn() call not taking effect
test('password', () => {
return anAsyncFunction().then(() => {
warn(); // 🚨
});
});
// ✔ Use useWarn() when you need to set warn after await
test('password', async () => {
const setWarn = useWarn();
await someAsyncFunction();
setWarn();
enforce(passwordStrength).isNotWeak();
});
```
### Using warning in the result object
Just like you do with errors, you can access the errors in your validation warnings using these methods:
```js
result.hasWarnings(); // Returns whether any warnings are present in the suite.
result.hasWarnings('password'); // Returns whether any warnings are present in the 'password' field.
result.getWarnings(); // Returns an object with all the fields that have warnings, and an array of warnings for each.
result.getWarnings('password'); // Returns an array of warnings for the password field.
```
**Read next about:**
- [Vest's result object](https://vestjs.dev/docs/writing_your_suite/accessing_the_result).
# Accessing Vest's Result
Vest validations return a results object that holds all the information regarding the current run and methods to interact with the data. You can access it in three ways:
- `const result = suite.run(data);` - runs the suite and returns the latest result (Promise-like when async).
- `const result = suite.get();` - returns the current result without running.
- `suite.hasErrors()`, `suite.isValid()`, etc. - selectors are also exposed directly on the suite.
import AccessingResultSandpack from '@site/src/components/Sandpack/AccessingResult';
```js
const result = suite.run(data);
```
:::note Async suites
When your suite contains async tests, the returned result is also a Promise. You can still read sync fields immediately, while pending async fields report `isPending('field') === true`.
:::
## Interactive Result Inspector
Use this playground to see how the result object properties change as you interact with the form.
## `run`
The `run` object contains metadata about the current suite execution.
```js
result.run.data; // { raw: ..., parsed: ... }
result.run.time; // Date object
result.run.focus; // { only: ..., skip: ..., onlyGroup: ..., skipGroup: ... }
```
### `run.focus`
Provides insight into the exact modifiers that were used to focus the current run. This is either populated by calling `.focus(...)` or `.only(...)`/`.skip(...)`.
```js
suite.only('username').run().run.focus;
// { only: ['username'] }
suite.focus({ skipGroup: 'groupA' }).run().run.focus;
// { skipGroup: ['groupA'] }
```
## `isValid`
`isValid` returns whether the validation suite as a whole or a single field is valid or not.
### Suite validity
A _suite_ is considered valid if the following conditions are met:
- There are no errors (`hasErrors() === false`) in the suite - warnings are not counted as errors.
- All non [optional](https://vestjs.dev/docs/writing_your_suite/optional_fields) fields have passing tests.
- There are no pending async tests.
```js
suite.isValid();
suite.get().isValid();
result.isValid();
```
### Field validity
A _field_ is considered valid if the following conditions are met:
- The field has no errors (`hasErrors() === false`) or the field is omitted via the functional "optional" API.
- All non-optional tests for the field are passing.
- The field has no pending tests.
```js
suite.isValid('username');
suite.get().isValid('username');
result.isValid('username');
```
:::tip NOTE
When `isValid` equals `false` it does not necessarily mean that the form is inValid. It only means that might not be valid _yet_. For example, if not all the fields are filled, the form is not valid yet, even though it may not be strictly invalid.
:::
## `hasErrors` and `hasWarnings`
If you only need to know if a certain field has validation errors or warnings but don't really care which they are, you can use `hasErrors` or `hasWarnings` functions.
```js
result.hasErrors('username');
// true
result.hasWarnings('password');
// false
```
In case you want to know whether the whole suite has errors or warnings (to prevent submit, for example), you can use the same functions, just without specifying a field
```js
result.hasErrors();
// true
result.hasWarnings();
// true
```
## `isValidByGroup`
Similar to `isValid`, but returns the result for a specified [group](https://vestjs.dev/docs/writing_tests/advanced_test_features/grouping_tests). Providing a group name that doesn't exist will return `false`. When adding a fieldName, only the field within that group will be checked.
```js
result.isValidByGroup('groupName', 'fieldName');
result.isValidByGroup('groupName');
```
### Return Value
Returns a boolean value, whether the group/field combo is valid or not.
### Parameters
| Parameter | Type | Required? | Description |
| --------- | ------ | --------- | -------------------------------------------------------------------------------------------------------- |
| groupName | string | Yes | Name of the group |
| fieldName | string | No | Name of the field. When specified, only the result for the specified field within the group is returned. |
## `hasErrorsByGroup` and `hasWarningsByGroup`
Similar to `hasErrors` and `hasWarnings`, but returns the result for a specified [group](https://vestjs.dev/docs/writing_tests/advanced_test_features/grouping_tests)
To get the result for a given field in the group:
```js
result.hasErrorsByGroup('groupName', 'fieldName');
// true
result.hasWarningsByGroup('groupName', 'fieldName');
// false
```
And to get the result for a whole group.
```js
result.hasErrorsByGroup('groupName');
// true
result.hasWarningsByGroup('groupName');
// true
```
[Read more about groups](https://vestjs.dev/docs/writing_tests/advanced_test_features/grouping_tests)
## `getError` and `getWarning`
:::warning WARNING
Both these functions may return undefined when no errors or warnings are present, so make sure to check for that if you're relying on their return value.
:::
### `getError()`
```typescript
// When no field name is provided: Gets the first error object
const firstError = result.getError();
console.log(firstError);
// Output: { fieldName: 'username', message: 'Username is required', groupName: undefined }
// When a fieldname is provided: Gets the first error message for the field
const usernameError = result.getError('username');
console.log(usernameError);
// Output: 'Username is required'
```
The `getError()` function allows you to retrieve the first error message of a given field. If a field name is not provided, it returns the first error object in the `errors` array.
If a field name is provided, it returns the first error message for that field, or `undefined` if there were no errors for that field. If no field name is provided, it returns the first error object in the `errors` array, or `undefined` if there were no errors.
#### Example
```js
const error = result.getError(); // get first error object
console.log(`Error on field ${error.fieldName}: ${error.message}`);
```
### `getWarning()`
```typescript
// When no field name is provided: Gets the first warning object in the suite
const firstWarning = result.getWarning();
console.log(
`First warning: ${firstWarning.fieldName} - ${firstWarning.message}`,
);
// When a field name is provided: Gets the first warning string for the field
const usernameWarning = result.getWarning('username');
console.log(`Warning for username field: ${usernameWarning}`);
```
The `getWarning()` function allows you to retrieve the first warning message of a given field. If a field name is not provided, it returns the first warning object in the `warnings` array.
If a field name is provided, it returns the first warning message for that field, or `undefined` if there were no warnings for that field.
## `getMessage`
`getMessage` returns the first error or warning message for a given field. If a given field has both errors and warnings, it will return the first error message.
## `getErrors` and `getWarnings`
These functions return an array of errors for the specified field. If no field is specified, it returns an object with all fields as keys and their error arrays as values.
```js
result.getErrors('username');
// ['Username is too short', `Username already exists`]
result.getWarnings('password');
// ['Password must contain special characters']
```
If there are no errors for the field, the function defaults to an empty array:
```js
result.getErrors('username');
// []
result.getWarnings('username');
// []
```
You can also call these functions without a field name, which will return you an array per field:
```js
result.getErrors();
// {
// username: ['Username is too short', `Username already exists`],
// password: ['Password must contain special characters']
// }
```
:::tip NOTE
If you did not specify error messages for your tests, your errors array will be empty as well. In such case you should always rely on `.hasErrors()` instead.
:::
## `getErrorsByGroup` and `getWarningsByGroup`
Just like get `getErrors` and `getWarnings`, but narrows the result to a specified [group](https://vestjs.dev/docs/writing_tests/advanced_test_features/grouping_tests).
```js
result.getErrorsByGroup('groupName', 'fieldName');
result.getWarningsByGroup('groupName', 'fieldName');
result.getErrorsByGroup('groupName');
result.getWarningsByGroup('groupName');
```
[Read more about groups](https://vestjs.dev/docs/writing_tests/advanced_test_features/grouping_tests).
## `.afterEach()` and `await suite.run()`
[Read the full guide on Handling Suite Completion](https://vestjs.dev/docs/writing_your_suite/handling_completion).
Use `.afterEach()` to register a callback that will be called after the initial sync completion and again after each async test finishes. This is the recommended way to handle completion logic, including async suites. You can also use `await suite.run()` to get the result when all tests are finished.
| Parameter | Type | Required? | Description |
| ------------------- | ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `callback` | `Function` | Yes | A callback to be run after each completion cycle. Use with `.afterEach(callback).run()` for completion logic. The callback receives no arguments. |
| `await suite.run()` | `Promise` | No | Returns a promise that resolves when the suite is done running. Use with async/await for modern async handling. |
If you need to check for completion of specific fields, do so inside your callback logic.
`.afterEach()` can be chained before calling `.run()`, and multiple callbacks can be registered if needed.
Example:
```js
import { create, test, enforce } from 'vest';
const suite = create(data => {
test(
'UserEmail',
'Marked as spam address',
async () => await isKnownSpammer(data.address),
);
test(
'UserName',
'must not be blacklisted',
async () => await isBlacklistedUser(data.username),
);
});
suite
.afterEach(() => {
const res = suite.get();
if (res.hasErrors('UserName')) {
showUserNameErrors(res.errors);
}
reportToServer(res);
promptUserQuestionnaire(res);
})
.run();
```
:::danger IMPORTANT
Do not use `.afterEach()` conditionally, especially with async tests. This might cause unexpected behavior or missed callbacks. Instead, perform your conditional logic within your callback.
:::
```js
// 🚨 This might not work as expected when working with async validations
if (field === 'username') {
suite
.afterEach(() => {
/*do something*/
})
.run();
}
```
```js
// ✅ Instead, perform your checks within your after callback
suite
.afterEach(() => {
/* ... */
if (field === 'username') {
/*do something*/
}
})
.run();
```
## `.afterField()`
Similar to `.afterEach()`, but runs when a specific field finishes validation.
```javascript
suite.afterField('username', () => {
const res = suite.get();
if (res.hasErrors('username')) {
// handle username errors
}
});
```
## isPending
Returns whether the suite, or a specific field are pending or not. A suite is considered pending if it has unresolved [async tests](https://vestjs.dev/docs/writing_tests/async_tests).
Returns `true` if the suite is pending, `false` otherwise.
```js
import { create, test } from 'vest';
const suite = create((data = {}) => {
test('username', 'Username is already taken', async () => {
await someServerCall();
});
});
// Hybrid result: sync selectors work immediately
const result = suite.run(); // Promise-like
if (result.isPending('username')) {
// show spinner while async test runs
}
await result; // resolves when async tests finish
```
## isTested
Returns whether a given field has been tested or not. A field is considered tested if it has at least one test that ran. Often used as a replacement for dirty checking.
Returns `true` if the field is tested, `false` otherwise.
```js
import { create, test, enforce } from 'vest';
const suite = create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
});
const result = suite.run();
result.isTested('username'); // true if username has been tested
```
# Handling User Interaction
A common challenge in form validation is "noise control." You don't want to scream errors at a user before they've even touched a field.
Traditionally, libraries use an `isDirty` flag to track if a user has modified a field. Since Vest is **UI-agnostic** (it doesn't touch your DOM or listen to events), it doesn't track "dirty" state for you.
Vest provides two tools for this: **`isTested()`** and **`suite.only()`**.
## 1. `isTested()`: The Vest Alternative to `isDirty`
When you want to decide _if_ you should show an error message, you usually want to know: "Has this field actually been validated yet?"
If a field hasn't been validated, it usually means the user hasn't interacted with it. Vest tracks this for you.
```javascript
const result = suite.get();
// Only show errors if the field has actually been tested
const shouldShowError =
result.hasErrors('username') && result.isTested('username');
if (shouldShowError) {
renderError(result.getErrors('username'));
}
```
This pattern ensures that empty, untouched fields don't show "Required" errors when the form first loads.
## 2. Validating on Interaction with `suite.only()`
When a user blurs a field or types, you often want to validate **only that specific field**, without affecting other fields.
`suite.only()` does exactly this — it tells Vest to run validations for a specific field, while preserving the results of everything else.
```javascript
// On Blur handler
function handleBlur(fieldName, formData) {
// 1. Tell Vest to focus ONLY on the blurred field
suite.only(fieldName).run(formData);
}
```
### Why use `suite.only()`?
- **Performance:** It skips expensive tests (like async checks) for fields the user isn't touching.
- **User Experience:** It updates the state for the current field without accidentally flagging other fields as "tested" or "invalid" before the user reaches them.
:::tip Real-World Pattern
Combine `suite.only()` with `isTested()` for the best UX:
- Use `suite.only(fieldName)` in your `onBlur` handler to validate only the current field
- Use `isTested(fieldName)` when rendering to decide whether to show errors
:::
## Complete Example
```javascript
import suite from './validation';
function Form() {
const [formData, setFormData] = useState({});
const [result, setResult] = useState(suite.get());
const handleChange = e => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleBlur = e => {
const { name } = e.target;
// Validate only the blurred field
const res = suite.only(name).run(formData);
setResult(res);
};
const handleSubmit = e => {
e.preventDefault();
// Validate all fields on submit
const res = suite.run(formData);
setResult(res);
if (res.isValid()) {
// Submit the form
}
};
// Only show error if field was tested
const showError = fieldName => {
return result.isTested(fieldName) && result.hasErrors(fieldName);
};
return (
);
}
```
## Summary
| Goal | Traditional Approach | Vest Approach |
| :--------------------------- | :---------------------------- | :----------------------------------- |
| **Did the user touch this?** | Check `field.isDirty` | Check `result.isTested('field')` |
| **Validate on Blur** | Call `validateField('field')` | Call `suite.only('field').run(data)` |
By combining `isTested()` (to hide premature errors) and `suite.only()` (to update specific fields), you get precise control over the user experience without tightly coupling your validation to the DOM.
## Related
- [Focused Updates](https://vestjs.dev/docs/writing_your_suite/focused_updates) - Deep dive into `suite.only()` and `suite.focus()`
- [Accessing the Result](https://vestjs.dev/docs/writing_your_suite/accessing_the_result) - Learn about `isTested()` and other result methods
import ExecutionModesSandpack from '@site/src/components/Sandpack/ExecutionModes';
# Execution Modes
Vest provides three modes of execution - `Eager`, `One` and `All`. The mode determines how Vest behaves when a validation error occurs during the execution of a test suite. They can be set using the `mode()` function within the `create()` function.
- **Eager**: Stops executing tests for a field once a test fails, improving testing speed by stopping after the first error.
- **All**: Continues validating all tests within a field, even if a test fails, and reports all failures within the test suite.
- **One**: Stops executing tests for a field once a test fails and skips all subsequent tests, regardless of the field, useful when knowing that at least one test has failed is sufficient.
## Eager Mode
`Eager` mode is the default mode of operation in Vest. When in this mode, Vest will stop executing tests for a given field once a test fails. This means that a failed test will immediately stop the execution of subsequent tests for the same field. In this way, Vest helps to speed up the testing process by stopping after the first error.
To set the mode to `Eager`, you don't need to do anything since this is already the default behavior.
## All Mode
`All` mode is the alternative mode of operation in Vest. In this mode, Vest continues validating all tests within a field, even if a test fails. This means that Vest will execute all tests and report all failures within the test suite.
To set the mode to `All`, you can use the `mode()` function within the `create()` function:
```js
import { create, test, mode, Modes } from 'vest';
const suite = create(() => {
mode(Modes.ALL); // set the mode to All
test('field_name', 'error_message_1', () => {
// validate field
});
test('field_name', 'error_message_2', () => {
// validate field
});
});
```
In the above example, if the first test fails, Vest will continue validating the second test and report both failures.
## Live example
## One mode
`One` mode is a mode of operation in Vest that is similar to `Eager` mode, but with a slight difference in behavior. In `One` mode, like `Eager` mode, Vest stops executing tests for a given field once a test fails. However, unlike `Eager` mode, `One` mode goes a step further and skips all the tests that follow a failing test, regardless of the field they belong to.
The purpose of `One` mode is to optimize the testing process when you don't need a detailed list of all the failed tests, but rather only need to know if at least one test has failed. This mode is particularly useful when performing server-side validation at the API level, where knowing that there is at least one validation failure is sufficient.
To set the mode to `One`, you can use the `mode()` function within the `create()` function:
```js
import { create, test, mode, Modes } from 'vest';
const suite = create(() => {
mode(Modes.ONE); // set the mode to One
// 🚨 If this test fails, all next tests will be skipped
test('field_1', 'error_message_1', () => {
/*...*/
});
test('field_2', 'error_message_2', () => {
/*...*/
});
});
```
In the above example, if the first test fails, Vest will immediately stop executing any remaining tests for the same field and also skip all tests for subsequent fields within the test suite.
# Focused Updates
Sometimes you want to run validation only for a specific field (e.g., on blur). Vest 6 introduces the `suite.focus()` method for declarative control over which fields to validate.
:::tip New in Vest 6
`suite.only()` and `suite.focus()` are the recommended ways to handle field-focused validation in Vest 6. They provide a cleaner API compared to using `only()` and `skip()` hooks inside your suite.
:::
## Why Focus?
In a large form, re-validating the entire suite on every keystroke can be inefficient and annoying for the user (e.g., showing errors for fields they haven't touched yet). Focused updates allow you to:
- **Validate on Blur**: Run checks only for the field the user just left.
- **Skip Expensive Tests**: Temporarily bypass heavy async validations when they aren't needed.
- **Skip Entire Groups**: Bypass whole sections of validation (e.g., skip "sign-up" validations when signing in).
- **Improve Performance**: Run only what's necessary.
- **Better UX**: Avoid showing errors for untouched fields.
## Basic Usage
### Running Only Specific Fields
The most common use case is validating a single field as the user types or leaves an input. You can use the `suite.only('fieldName')` shorthand to restrict the run to specific fields.
(For multiple fields, you can pass an array: `suite.only(['field1', 'field2'])`).
If you need to combine focusing with other modifiers (e.g. `only` + `skipGroup`), use `suite.focus({ ... })` instead.
import FocusedUpdatesSandpack from '@site/src/components/Sandpack/FocusedUpdates';
### Skipping Specific Fields
Use `focus({ skip: ... })` to exclude specific fields from the run while running everything else.
```javascript
// Skip the 'promoCode' field during this run
suite.focus({ skip: 'promoCode' }).run(formData);
// Skip multiple fields
suite.focus({ skip: ['promoCode', 'referralCode'] }).run(formData);
```
### Skipping Entire Groups
Use `focus({ skipGroup: ... })` to skip all tests inside a named group. Tests outside the group run normally.
### Running Only Entire Groups
Use `focus({ onlyGroup: ... })` to run _only_ tests inside a named group. Tests outside the group (including top-level tests) are skipped.
```javascript
// Run only tests declared inside group('signUp', ...)
suite.focus({ onlyGroup: 'signUp' }).run(formData);
// Run only tests inside multiple specified groups
suite.focus({ onlyGroup: ['signIn', 'signUp'] }).run(formData);
```
### `skip` vs `skipGroup`
Both modifiers exclude tests, but they target different scopes:
- `skip`: skips by **field name**, no matter where that field is declared.
- `skipGroup`: skips by **group name**, regardless of field names inside that group.
```javascript
// Skip every `email` test across the suite (top-level and inside groups)
suite.focus({ skip: 'email' }).run(formData);
// Skip only tests declared inside group('signUp', ...)
suite.focus({ skipGroup: 'signUp' }).run(formData);
```
```javascript
import { create, test, group, enforce } from 'vest';
const suite = create(data => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
group('signIn', () => {
test('password', 'Password is required', () => {
enforce(data.password).isNotBlank();
});
});
group('signUp', () => {
test('email', 'Email is required', () => {
enforce(data.email).isEmail();
});
test('tos', 'You must accept the terms', () => {
enforce(data.tos).equals(true);
});
});
});
// When signing in, skip the signUp group entirely
suite.focus({ skipGroup: 'signUp' }).run(formData);
// When signing up, skip the signIn group entirely
suite.focus({ skipGroup: 'signIn' }).run(formData);
// Skip multiple groups at once
suite.focus({ skipGroup: ['signIn', 'signUp'] }).run(formData);
```
`skipGroup` works by injecting a `skip(true)` call at the beginning of each matching group's callback. Since `skip(true)` creates a transient isolate, it adds zero overhead to the stored suite state between runs.
### Combining Modifiers
You can combine `only`, `skip`, and `skipGroup` in a single `focus()` call:
```javascript
// Only validate 'username', and also skip the 'signUp' group
suite.focus({ only: 'username', skipGroup: 'signUp' }).run(formData);
```
### Focus Modifier Precedence
When multiple modifiers are used, they are evaluated in the following order of precedence (highest to lowest):
1. `skipGroup` (Destructive): Explicitly skipped groups are always skipped.
2. `onlyGroup` (Constructive): If present, restricts execution to specific groups. Top-level tests are excluded.
3. `skip` (Destructive): Explicitly skipped fields are skipped, even if they match an allowed group.
4. `only` (Constructive): If present, restricts execution to specific fields within the allowed groups.
> **Note**: A test is run only if it passes _all_ active filters. For example, if you use `onlyGroup: 'A'` and `skip: 'field1'`, `field1` inside `Group A` will be skipped.
## Fluent Chain API
`focus()` returns a "runnable" interface, allowing you to chain it with `afterEach`, `afterField`, or `run`.
```javascript
suite
.only('email')
.afterEach(() => updateUI(suite.get()))
.run(formData);
// Or with afterField for specific field callbacks
suite
.only(['email', 'password'])
.afterField('email', () => validateEmailUI(suite.get()))
.afterField('password', () => validatePasswordUI(suite.get()))
.run(formData);
```
## Real-World Examples
### Form Field Validation on Blur
```javascript
// In your form component
function handleBlur(fieldName, formData) {
suite
.only(fieldName)
.afterEach(() => setValidationResult(suite.get()))
.run(formData);
}
// Usage in React
handleBlur('email', formData)}
onChange={handleChange}
/>;
```
### Multi-Step Form with Group Skipping
```javascript
const suite = create(data => {
group('step1', () => {
test('name', 'Name is required', () => {
enforce(data.name).isNotBlank();
});
});
group('step2', () => {
test('address', 'Address is required', () => {
enforce(data.address).isNotBlank();
});
});
group('step3', () => {
test('payment', 'Payment method is required', () => {
enforce(data.payment).isNotBlank();
});
});
});
// Validate only step 2 by skipping the other steps
suite
.focus({ skipGroup: ['step1', 'step3'] })
.afterEach(() => setResult(suite.get()))
.run(formData);
```
### Validating All Fields Without Focus
When you need to validate everything (e.g., on form submit), simply call `run()` without `focus()`:
```javascript
// Validate all fields on submit
function handleSubmit(formData) {
suite.afterEach(() => setResult(suite.get())).run(formData);
}
// Or for focused blur validation
function handleBlur(fieldName, value) {
suite
.only(fieldName)
.afterEach(() => setResult(suite.get()))
.run({ ...formData, [fieldName]: value });
}
```
### React Hook Integration
```jsx
import { useState, useCallback } from 'react';
import { create, test, enforce } from 'vest';
import 'vest/email';
const suite = create(data => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
test('email', 'Email must be valid', () => {
enforce(data.email).isEmail();
});
});
function useFormValidation(initialData) {
const [formData, setFormData] = useState(initialData);
const [result, setResult] = useState(suite.get());
const validateField = useCallback(
fieldName => {
suite
.only(fieldName)
.afterEach(() => setResult(suite.get()))
.run(formData);
},
[formData],
);
const validateAll = useCallback(() => {
suite.afterEach(() => setResult(suite.get())).run(formData);
}, [formData]);
return { formData, setFormData, result, validateField, validateAll };
}
```
## Focus Modifiers Reference
| Modifier | Type | Description |
| ----------- | -------------------- | ----------------------------------------------------------------------------------- |
| `only` | `string \| string[]` | Run only the specified field(s). All others are excluded. |
| `skip` | `string \| string[]` | Skip the specified field(s). All others run as usual. |
| `onlyGroup` | `string \| string[]` | Run only tests inside the named group(s); top-level (ungrouped) tests are excluded. |
| `skipGroup` | `string \| string[]` | Skip all tests inside the named group(s). |
## Comparison: `suite.focus()` vs `only()`/`skip()`
| Feature | `only()`/`skip()` | `suite.focus()` |
| -------------------------- | ------------------------------ | -------------------------------------- |
| **Location** | Inside suite callback | Outside, at call site |
| **Flexibility** | Requires conditional logic | Fully dynamic |
| **Separation of Concerns** | Mixed with validation | Decoupled from validation |
| **Group Skipping** | `skip(true)` inside group | `skipGroup` modifier |
| **Chainable** | No | Yes (`afterEach`, `afterField`, `run`) |
| **Best For** | Static, logic-based exclusions | UI-driven field focus |
### When to Use Each
**Use `suite.focus()`** when:
- Validating on blur or focus events
- The decision of what to validate comes from UI interactions
- You want to skip entire groups from outside the suite
- You want to chain callbacks
**Use `only()`/`skip()`** when:
- The exclusion logic depends on the data itself
- You have static, predetermined exclusions
- The logic belongs inside the suite
## Behavior Notes
:::caution Important
- **Non-persistent**: Focused runs do not persist between calls. Each `focus` call applies only to the immediately following `run()`.
- **Schema Validation**: When focusing specific fields, schema validation is skipped for fields outside the focus scope, allowing targeted validation even if the full payload is invalid.
- **State Preservation**: Previous validation results for non-focused fields are preserved.
- **skipGroup is transient**: The `skip(true)` isolate injected by `skipGroup` is transient — it is not stored in the suite state tree and adds zero overhead between runs.
:::
## TypeScript Support
`suite.focus()` is fully typed. The field names are inferred from your suite definition:
```typescript
interface FormData {
username: string;
email: string;
password: string;
}
const suite = create((data: FormData) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
// ...
});
// TypeScript will autocomplete field names
suite.only('username').run(formData); // ✅
suite.only('nonexistent').run(formData); // ❌ Type error
suite.focus({ skip: 'email' }).run(formData); // ✅
suite.focus({ skipGroup: 'myGroup' }).run(formData); // ✅
suite.focus({ skipGroup: ['groupA', 'groupB'] }).run(formData); // ✅
suite.focus({ onlyGroup: 'groupA' }).run(formData); // ✅
```
## Related
- [Including and Excluding Fields](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/skip_and_only) - Using `only()` and `skip()` inside suites
- [Include](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/include) - Link related fields to run together
- [Test Groups](https://vestjs.dev/docs/writing_tests/advanced_test_features/grouping_tests) - Grouping tests together
- [`focus({ skip / skipGroup / onlyGroup })` Patterns](https://vestjs.dev/docs/recipes/focus_skipgroup_recipes) - Practical recipes for choosing and combining focus modifiers
# Handling Suite Completion
Vest suites run synchronously by default, but they often contain asynchronous tests (e.g., checking if a username is taken). Vest provides two main ways to handle the completion of your suite:
1. **Promises (Recommended)**: `await suite.run()`
2. **Event Callbacks**: `suite.afterEach()` and `suite.afterField()`
## 1. Promise-based Handling (Recommended)
For most modern applications, simply awaiting the result is the cleanest way to handle async validations. The result object returned by `suite.run()` implements the Promise interface, so you can `await` it directly.
The promise resolves **once**, when **all** tests (both synchronous and asynchronous) have finished.
```javascript
const result = await suite.run(formData);
if (result.isValid()) {
// All tests passed! Submit the form.
submitForm(formData);
} else {
// Handle errors
showErrors(result.getErrors());
}
```
### When to use it?
- When you need to block an action (like form submission) until validation is complete.
- When you want linear, readable code using `async/await`.
## 2. Using `suite.afterEach(callback)`
If you prefer an event-driven approach, or if you need to react to updates _during_ the validation process (e.g., updating the UI as individual async tests complete), use `suite.afterEach()`.
`suite.afterEach()` registers a callback that runs after the initial sync completion and again after each async test finishes. It is chainable and can be attached before or during the run.
### Key Behavior
Unlike a Promise which resolves only once, **`afterEach` callbacks may run multiple times**:
1. It runs **immediately** after the synchronous pass.
2. It runs **again** whenever an async test completes.
This makes it perfect for keeping your UI in sync with the validation state.
```javascript
suite
.afterEach(() => {
// Called when the suite finishes sync execution
// AND whenever an async test completes
updateUI(suite.get());
})
.run(data);
```
### When to use it?
- When you want to update the UI reactively as validation progresses.
- When you are using a framework that relies on callbacks or subscriptions.
## 3. Using `suite.afterField(fieldName, callback)`
Sometimes you only care about the completion of a specific field. `suite.afterField()` is a specialized hook that runs whenever a test for a specific field finishes running.
If a field has multiple asynchronous tests, this callback will run multiple times (once for each completing test).
This is highly useful for UI patterns like removing a loading spinner from a specific input "on blur" or when its validation completes.
```javascript
suite
.afterField('username', () => {
// Runs when a test for 'username' finishes
setLoading('username', false);
const res = suite.get();
if (res.hasErrors('username')) {
showUsernameError(res.getErrors('username'));
}
})
.run(data);
```
## 4. Chaining Methods
Both `afterEach` and `afterField` return the suite API, allowing for fluent chaining.
```javascript
suite
.afterEach(updateGeneralUI)
.afterField('email', handleEmailCompletion)
.afterField('username', handleUsernameCompletion)
.run(data);
```
## Migration from V5
In Vest 5, the `done()` method was used on the result object. In Vest 6, this has been moved to the suite object itself for better ergonomics and chaining support.
| Vest 5 | Vest 6 |
| :---------------------- | :------------------------------------ |
| `res.done(cb)` | `suite.afterEach(cb).run()` |
| `res.done('field', cb)` | `suite.afterField('field', cb).run()` |
# Conditionally Running tests together - linked fields
There are scenarios in which we want to conditionally run tests on fields that are not the ones the user is currently interacting with. The most common case is when we have fields that depend on one another, and when changing one, the other should re-run its validation as well. For example, when we have two number fields, and we want to make sure that one is lower than the other. When the user interacts with one, we want to make sure the other one is revalidated.
This can be done with `include` and its `when` modifier.
When applied by itself, `include` behaves as an addition to only and not as a substitute for it. It takes a field name to include, if there are no other criteria (such as skip or skipWhen applied) that cause the field to be skipped, it will be run.
```js
include('fieldName');
```
`include` also has a `.when` modifier, that can add more specific criteria to when the field should be included. The when modifier can be passed one of the following:
| type | description |
| ---------- | ----------------------------------------------------------------------------------------------------------- |
| `string` | A name of a different field. When that field is included via `only()`, this field will be included as well. |
| `function` | A function that returns a boolean. When the function returns true, the field will be included. |
| `boolean` | A boolean. When true, the field will be included. |
```js
include('confirm').when('password'); // Will include "confirm" when we have `only('password')`
```
```js
include('confirm').when(someValue); // Will include "confirm" when `someValue` is `true`
```
```js
include('confirm').when(() => someValue); // Will include "confirm" when the callback returns true
```
:::tip Note
All these will only applied if the field is not skipped directly, or excluded because it is in a skipped grouppkip.
:::
When using the function modifier, the function is evaluated each time a matching field is observed, so if we have multiple tests with the same name, the callback will be checked each time.
The function modifier gets as its argument the current suite result object, so we can include fields based on the current result.
```js
include('username').when(result => result.hasErrors('username'));
```
## Usage Examples
```js
create((data = {}, currentField) => {
only(currentField); // 'password'
include('confirm').when('password'); // 'confirm' will also run if `currentField` is 'password'
test('password', 'password is required', () => {
enforce(data.password).isNotBlank();
});
test('confirm', 'Passwords do not match', () => {
enforce(data.confirm).equals(data.password);
});
});
```
In the example above you'll see that we're running the validation for `confirm` both when `currentField` is `password` and when it is `confirm`. This is useful, but you'll quickly see that it has its limitations. For example, what if the user did not fill in the `confirm` field? We'll get an error, even though we should probably wait for the confirm field to be filled in.
This is why we also support the more verbose functional `when` condition that allows you to specify a function that returns a boolean, and gives you more control over when the field is included.
```js
create((data = {}, currentField) => {
only(currentField); // 'password'
include('confirm').when(() => currentField === 'password' && data.confirm);
// 'confirm' will also run when `currentField` is 'password' and `data.confirm` is not empty
test('password', 'password is required', () => {
enforce(data.password).isNotBlank();
});
test('confirm', 'Passwords do not match', () => {
enforce(data.confirm).equals(data.password);
});
});
```
# omitWhen - Conditionally omit tests from a suite
In some cases, we need to wish to omit certain portions of our suite in a way that these tests won't run, and will not count against `isValid`. For example, when we have some tests that are only allowed to run when a certain checkbox is checked by the user.
Generally, when we skip fields, they are counted against `isValid`, meaning, unless specifically marked as `optional`, the suite will not be regarded as valid. Using `omitWhen` fixes it by both preventing the omitted tests from running, _and_ allowing the suite to be valid even without them.
## Differences from `skipWhen`
Unlike `skipWhen`, tests omitted using `omitWhen` are not counted against `isValid`. Additionally, any validation message of an omitted test will be excluded from the suite result if the condition for the `omitWhen` is true.
## Params
| Name | Type | Description |
| :---------- | :--------------------- | :---------------------------------------------------------------------------------------------------- |
| Conditional | `boolean`/`function`\* | The conditional expression to be evaluated. When Truthy, the tests within `omitWhen` will be omitted. |
| Body | `function` | A callback function containing the tests to either omit or run. |
\* When using the function conditional, the function will be passed the current validation result as an argument, so it can be used to skip tests based on the current validation result.
## Usage Example
```js
import { create, test, enforce, omitWhen, only } from 'vest';
create((data = {}, currentField) => {
only(currentField);
test('username', 'username is required', () => {
enforce(data.username).isNotBlank();
});
omitWhen(data.useNewAddress, () => {
test('address_line_1', 'Address Line 1 is required', () => {
enforce(data.address_line_1).isNotBlank();
});
test('city', 'City is required', () => {
enforce(data.city).isNotBlank();
});
test('postal_code', 'Postal code is required', () => {
enforce(data.postal_code).isNotBlank();
});
});
});
```
```js
omitWhen(
res => res.hasErrors('username'),
() => {
test('username', 'Username already exists', () => {
// this is an example for a server call
return doesUserExist(data.username);
});
},
);
```
```js
omitWhen(
(value, values) => {
if (!values) {
return false;
}
return values.length === 1;
},
({ value, values, field }) => {
if (!values) {
return false;
}
if (values.length === 1 && values[0] === value) {
return true;
}
return false;
},
'You need at least one option',
);
```
:::info IMPORTANT
The code within `omitWhen` will only run if the condition is not met. If the condition is truthy, `omitWhen` skip calling the callback altogether, and the tests within it will not be executed or registered.
```js
omitWhen(true, () => {
console.log('This will NOT run');
});
```
:::
# skipWhen: Conditional Exclusion
Sometimes, you might need to skip a test or a group based on a condition. For instance, you might need to skip tests based on the intermediate state of the currently running suite. In such cases, you can use `skipWhen`, which is a helper function that takes a boolean expression and a callback with tests. If the expression evaluates to `true`, the tests within the callback will be skipped, and if it's `false`, the tests will run as normal.
## Parameters
The skipWhen function takes the following parameters:
| Name | Type | Description |
| :---------- | :--------------------- | :---------------------------------------------------------------------------------------------------- |
| Conditional | `boolean`/`function`\* | The conditional expression to be evaluated. When Truthy, the tests within `skipWhen` will be skipped. |
| Body | `function` | A callback function containing the tests to either skip or run. |
\* When using the function conditional, the function will be passed the current validation result as an argument so that it can be used to skip tests based on the current validation result.
:::tip Note
When using `skipWhen`, the tests within the block will be skipped, but will still be counted against `isValid`. As long as the tests don't run, the suite will not be marked as valid. If you wish these tests to override suite validity, use [`omitWhen`](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/omitWhen) instead.
:::
## Usage Example
In the following example, we're skipping the server side verification of the username if the username is invalid to begin with:
```js
import { create, test, enforce, skipWhen } from 'vest';
export default create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
skipWhen(
res => res.hasErrors('username'),
() => {
test('username', 'Username already exists', async () => {
// this is an example for a server call
return doesUserExist(data.username);
});
},
);
});
```
In this example, we first validate that the `username` field is not empty. If it's empty, we report an error. Next, we use `skipWhen` to skip the verification of the username on the server-side if the `username` field has an error. If the `username` field does not have an error, we perform the server-side verification using a function called `doesUserExist`, which returns a promise that resolves to `true` if the username already exists, or `false` otherwise.
Using `skipWhen` allows us to conditionally skip tests, depending on the state of the validation result. In this case, it helps us avoid unnecessary server-side requests if the `username` field is empty, or if it already has an error.
:::caution IMPORTANT
The code within skipWhen will always run, regardless of whether the condition is met or not. `skipWhen` only affects the validation result, but if you call a function within `skipWhen`, it will run regardless of the condition.
```js
skipWhen(true, () => {
console.log('This will always run');
});
```
:::
import SkipAndOnlySandpack from '@site/src/components/Sandpack/SkipAndOnly';
# Including or Excluding Fields in Vest Validation Framework
In real-world scenarios, you may need to run tests only on a specific field or skip some tests according to some logic. To handle such cases, Vest includes `skip()` and `only()` functions.
`skip()` and `only()` functions can exclude or include specific fields from being validated. These functions should be called from the body of suite callback and should be called before anything else to take effect.
Both skip and only can be used as many times as needed, and they can be nested within groups. skip and only will only take effect on the scope their declared in (and below), and only affect tests that are declared after their invocation.
:::caution
`skip()` and `only()` should not be called conditionally - i.e. inside of an if statement. Vest relies on the consistent execution of these functions in the suite to detect changes between runs.
Instead, you can conditionally supply your skip and only arguments. For example:
```js
skip(shouldSkip ? 'field1' : false);
```
```diff
- if (hasPromo) only('promo');
+ only(hasPromo && 'promo');
```
:::
## Skip and Only Arguments
Skip and Only take one argument, which can be one of the following:
- A string representing a field name
- An array of strings representing multiple field names
- `undefined` or `false` to prevent execution of the function
- **SKIP ONLY** `true` to skip or include all fields
## Only Running Specific Fields
When validating user interactions, you usually want to validate only the field that the user is currently interacting with, to prevent errors appearing for untouched inputs. You can use `only()` with the name of the test currently being validated to achieve this.
In the following example, we assume that the argument `fieldName` is being populated with the name of the field we want to test. If none is passed, the call to `only()` will be ignored, and all tests will run as usual. This allows us to test each field at a time during the interaction but test all on form submission.
```js
import { create, test, only } from 'vest';
const suite = create((data, fieldName) => {
only(fieldName);
test('username', 'Username is invalid', () => {
/* some validation logic*/
});
test('email', 'Email is invalid', () => {
/* some validation logic*/
});
test('password', 'Password is invalid', () => {
/* some validation logic*/
});
});
const validationResult = suite.run(formData, changedField);
```
:::tip Linking Related Fields to Run Together
You can make fields run together by using [include](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/include). This is useful when you have fields that depend on each other, and you want to make sure they run at the same time.
:::
## V6 Recommended: Using `suite.only()` and `suite.focus()`
In Vest 6, the preferred way to focus validation on specific fields is to use the **`suite.only()`** and **`suite.focus()`** methods. This approach is cleaner and separates the "what to validate" from "how to validate".
```javascript
import { create, test, enforce } from 'vest';
const suite = create(data => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
test('email', 'Email is required', () => {
enforce(data.email).isEmail();
});
test('password', 'Password is required', () => {
enforce(data.password).longerThanOrEquals(8);
});
});
// Focus on a single field
suite.only('username').run(formData);
// Focus on multiple fields
suite.only(['username', 'email']).run(formData);
// Skip specific fields
suite.focus({ skip: 'password' }).run(formData);
// Skip entire groups
suite.focus({ skipGroup: 'signUp' }).run(formData);
// Chain with afterEach for callbacks
suite
.only('email')
.afterEach(() => updateUI(suite.get()))
.run(formData);
```
### `suite.focus()` Modifiers
| Modifier | Type | Description |
| ----------- | -------------------- | --------------------------------------------------------- |
| `only` | `string \| string[]` | Run only the specified field(s). All others are excluded. |
| `skip` | `string \| string[]` | Skip the specified field(s). All others run as usual. |
| `skipGroup` | `string \| string[]` | Skip all tests inside the named group(s). |
### Why Use `suite.only()` Over `only()`?
| Feature | `only()` (inside suite) | `suite.only()` / `suite.focus()` (outside) |
| -------------------------- | --------------------------- | ------------------------------------------ |
| **Declaration** | Inside suite callback | At call site |
| **Flexibility** | Must be conditional | Fully dynamic |
| **Separation of Concerns** | Mixed with validation logic | Decoupled from validation |
| **Chainable** | No | Yes (returns runnable API) |
| **Best For** | Static exclusions | UI-driven field focus |
> **Recommendation**: Use `suite.only()` or `suite.focus()` for runtime decisions (e.g., validating on blur), and `only()`/`skip()` for static, logic-based exclusions inside your suite.
When choosing between modifiers in `suite.focus()`, prefer `skipGroup` when your intent is to disable a named validation section (for example `signUp`), and prefer `skip` when your intent is to exclude a specific field everywhere it appears.
For more details, see [Focused Updates](https://vestjs.dev/docs/writing_your_suite/focused_updates).
## Interactive example
## Skipping fields
There are cases when you may need to skip specific tests. For example, when you wish to prevent validation of a promo code when none provided. You can use the `skip()` function to skip the specified fields. All other tests will run as usual.
```js
import { create, test, skip } from 'vest';
const suite = create(data => {
skip(data.promo ? 'promo' : false); // will skip the promo test if data.promo is falsy
// this test won't run when data.promo is falsy.
test('promo', 'Promo code is invalid', () => {
/* some validation logic*/
});
});
const validationResult = suite.run(formData);
```
By using `skip()` and `only()` functions in Vest, you can easily exclude or include fields from being validated, making your validation process more efficient and effective.
## Skipping or Including tests in a group
You can also use `skip()` and `only()` to skip or include tests in a group.
```js
import { create, group, test, skip, only } from 'vest';
const suite = create(data => {
group('sign-in', () => {
skip('password');
test('username', 'Username is invalid', () => {
/* ... */
});
test('password', 'Password is invalid', () => {
/* ... */
}); // This will be skipped
});
});
```
```js
import { create, group, test, skip, only } from 'vest';
const suite = create(() => {
group('sign-in', () => {
only('password');
test('username', 'Username is invalid', () => {
/* ... */
}); // This will be skipped
test('password', 'Password is invalid', () => {
/* ... */
});
});
});
```
## Skipping an entire group
You can also skip an entire group by passing a `true` to `skip()`.
```js
import { create, group, test, skip, only } from 'vest';
const shouldSkipSignIn = true;
const suite = create(data => {
group('sign-in', () => {
skip(shouldSkipSignIn);
// now all tests in this group will be skipped
test('username', 'Username is invalid', () => {
/* ... */
});
test('password', 'Password is invalid', () => {
/* ... */
});
});
});
```
## Combining Multiple Nesting Levels of Skip/Only
You can combine multiple nesting levels of skip() and only() to further refine which fields and tests are included or excluded. When skip() or only() is called at a higher level in the nesting hierarchy, it will affect all nested scopes unless overridden by subsequent skip() or only() calls.
```js
import { create, group, test, skip, only } from 'vest';
const suite = create(() => {
only('field1');
test('field1', 'Field 1 is invalid', () => {
/* ... */
});
group('nested-group', () => {
skip('field2');
test('field1', 'Field 1 is invalid', () => {
/* ... */
});
test('field2', 'Field 2 is invalid', () => {
/* ... */
}); // This test will be skipped
});
});
```
In the above example, the only('field1') call at the top-level scope will only run tests for 'field1', including those within the nested group. However, the subsequent skip('field2') call within the nested group will skip the 'field2' test, overriding the only('field1') behavior.
By leveraging the flexibility of combining multiple levels of skip() and only(), you can precisely control which fields and tests are included or excluded, tailoring the validation process to your specific requirements.
Related: [include](https://vestjs.dev/docs/writing_your_suite/including_and_excluding/include)
import OptionalFieldsSandpack from '@site/src/components/Sandpack/OptionalFields';
# Optional Fields
By default, all tests inside Vest are required for the suite to be considered "valid". However, there may be situations in which some tests can be skipped, such as when dealing with optional fields in your application's logic. In such cases, Vest provides the optional function, which allows you to mark fields as optional, so that they can be skipped during validation without affecting the overall validity of the suite.
An optional field is a field that can be omitted or left empty during validation without causing the entire suite to be considered invalid. When a field is marked as optional using the optional function, Vest will exclude it from the validation process unless specified otherwise.
Vest has a best-effort approach to determining whether an optional field should be applied.
## How Vest Determines Optional Fields
When a field is marked as optional using the `optional` function, Vest will consider the following:
## If the tests never ran
If the field was skipped in all runs of the suite, it will be considered as optional.
```js
import { create, optional, only, test, enforce } from 'vest';
const suite = create((data = {}, currentField) => {
only(currentField); // only validate this specified field
optional(['pet_color', 'pet_age']);
test('pet_name', 'Pet Name is required', () => {
enforce(data.pet_name).isNotEmpty();
});
test('pet_color', 'If provided, pet color must be a string', () => {
enforce(data.pet_color).isString();
});
test('pet_age', 'If provided, pet age must be numeric', () => {
enforce(data.pet_age).isNumeric();
});
});
suite.run({ pet_name: 'Indie' }, 'pet_name').isValid(); // ✅ Since pet_color and pet_age are optional, the suite may still be valid
suite.run({ pet_age: 'Five' }, 'pet_age').isValid(); // 🚨 When erroring, optional fields still make the suite invalid
```
## If the field is empty in the data object
If the first argument in the suite params is an object with the optional field as a key, and the value is `undefined`, `null`, or an empty string, it will be considered as optional.
:::caution NOTE
If the field is not present in the data object, or the first parameter is not the data object, Vest will default to the first option.
:::
```js
const suite = create(data => {
optional('age');
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
test('age', 'Age is invalid', () => {
enforce(data.age).isNumber();
});
});
const result = suite.run({
username: 'John',
age: '', // age is empty
});
result.isValid();
// ✅ Since we marked age as optional, the suite may still be valid
```
## Custom Omission Rules
Since every app is different, your app's logic may require some other definition of optional. For example, the user may have typed inside a field and then removed its content. In such cases, you can provide `optional` with a custom optional logic.
### Providing the field value for automatic omission
As mentioned in the previous section, Vest will try to determine whether a field should be omitted based on the value of the field in the data object. If the field value is coming from a different source, or is not the same key as its name in your tests (for example: `username` and `user_name`), you can supply the correct field value to Vest. If that value is blank (`''`, `null`, or `undefined`), the field will be omitted.
```js
const suite = create(data => {
optional({
// Here we tell vest to use the value of `user_name` instead of `username`
// to determine if the field should be omitted.
username: data.user_name,
});
test('username', 'Username is too short', () => {
enforce(data.user_name).longerThanOrEquals(3);
});
});
```
## Providing a function for custom omission
Sometimes the logic for optional field is more complex, for example - whether some other field has errors, or based on some other computed logic. In such cases, you can provide a function to Vest, which will be called with the data object and the current field name, and should return a boolean value indicating whether the field should be omitted.
The following code demonstrates how to allow a field to be empty if a different field is filled:
```js
const suite = create(data => {
optional({
pet_name: () => !suite.get().hasErrors('owner_name'),
owner_name: () => !suite.get().hasErrors('pet_name'),
});
test(
'pet_name',
'Pet Name may be left empty only if owner name is supplied',
() => {
enforce(data.pet_name).isNotEmpty();
},
);
test(
'owner_name',
'Owner Name may be left empty only if pet name is supplied',
() => {
enforce(data.owner_name).isNotEmpty();
},
);
});
```
## Live example
## Difference between `optional` and `warn`
The difference between `optional` and `warn` is significant, despite their similar appearance. While `optional`, like `only` and `skip`, is applied to the entire field, making all tests optional, `warn` is set at the test level and only affects specific tests marked with the `warn` option.
Another notable distinction is that tests marked as warnings do not make the suite invalid.
In some rare instances, you might have a field that is both optional and a warning. In these cases, you can combine the two options.
# Schema Validation
Vest introduces optional schema validation using `n4s` (enforce).
## Why use a Schema?
Validating data structure is often the first step in any validation pipeline. Before checking _if_ a username is available, you want to know that the `username` field actually exists and is a string.
Vest's schema support gives you:
1. **Type Safety**: Automatically infers TypeScript types for your data, so you get autocomplete and error checking in your suite.
2. **Structural Integrity**: Ensures your data matches the expected shape before running more complex validations.
3. **Fail Fast**: If the data structure is wrong, Vest fails immediately, saving resources.
## Defining a Schema
Use `enforce.shape`, `enforce.loose`, or `enforce.partial` to define your data structure.
```javascript
import { create, test, enforce } from 'vest';
const userSchema = enforce.shape({
username: enforce.isString(),
age: enforce.isNumber(),
email: enforce.optional(enforce.isString()), // Optional field
});
const suite = create(data => {
// `data` is typed: { username: string, age: number, email?: string | undefined }
test('username', 'Must be at least 3 chars', () => {
enforce(data.username).longerThan(2);
});
}, userSchema);
```
## How it works
When you pass a schema to `create`:
1. Vest implicitly runs the schema validation _before_ your tests.
2. If the data structure doesn't match the schema (e.g., `age` is a string instead of a number), the suite run fails immediately for those fields.
3. Your tests run assuming the data types are correct.
## TypeScript Inference for `create`
When a schema is passed as the second argument to `create`, Vest infers the suite callback data type and `run(...)` payload type directly from that schema.
```typescript
const userSchema = enforce.shape({
username: enforce.isString(),
age: enforce.isNumber(),
});
const suite = create(data => {
// data is inferred as: { username: string; age: number }
test('username', () => {
enforce(data.username).isNotBlank();
});
}, userSchema);
// `run` payload is typed from schema
suite.run({ username: 'john', age: 42 });
// TypeScript error: `age` must be a number
// suite.run({ username: 'john', age: '42' });
```
### Input vs output types with parsers
When a schema uses [data parsers](https://vestjs.dev/docs/enforce/builtin-enforce-plugins/data_parsers), Vest distinguishes between the **input type** (what `suite.run()` accepts) and the **output type** (what the callback receives and what `result.value` contains).
```typescript
const schema = enforce.shape({
age: enforce.isNumeric().toNumber(), // input: string | number, output: number
name: enforce.isString().trim().toUpper(), // input: string, output: string
});
const suite = create(data => {
// data.age is typed as `number` (the output type)
test('age', () => {
enforce(data.age).greaterThan(0);
});
}, schema);
// suite.run() accepts `string | number` for age (the input type)
suite.run({ age: '25', name: ' alice ' }); // ✅ No type error
const result = suite.run({ age: '25', name: ' alice ' });
result.value; // typed as { age: number; name: string }
```
The first rule in a chain determines the input type, and the last parser in the chain determines the output type. This means you never need `@ts-expect-error` or `as any` for valid parser coercion inputs.
### What becomes typed from the schema
With `create(callback, schema)`, TypeScript narrows:
- callback data (`data`) to the schema input shape.
- `suite.run(...)` / `suite.runStatic(...)` first argument to the schema input shape.
- the Standard Schema `~standard.validate(...)` input and output types.
- field-oriented happy-path APIs (`test`, `optional`, `include`) to schema keys.
- `result.types.input` and `result.types.output` to schema input/output types.
Some lifecycle/focus helpers (`remove`, `resetField`, `afterField`, `only`, `focus.only`) intentionally still accept dynamic strings for nested/dynamic runtime workflows.
Group modifiers (`onlyGroup` / `skipGroup`) remain `string` unless you explicitly provide group generics to `create`.
### API coverage (current typing standard)
When using `create(callback, schema)`, the current TypeScript standard is:
- Field-key inferred from schema for:
- `test(fieldName, message?, callback)`
- `include(fieldName).when(condition)`
- `optional(fieldName)`
- Group generic-aware (when explicitly provided):
- `group(groupName, callback)`
- `suite.focus({ onlyGroup / skipGroup })`
- Intentionally dynamic string-friendly:
- `suite.remove(fieldName)`
- `suite.resetField(fieldName)`
- `suite.only(fieldName)`
- `suite.afterField(fieldName, callback)`
- `only(fieldName)` / `skip(fieldName)` hooks
### Explicit generic override (advanced)
If needed, you can still provide explicit suite generics to fully control field/group names:
```typescript
const suite = create<'username' | 'age', 'account'>(data => {
// Without a schema, `data` is intentionally untyped (effectively `any`).
test('username', () => {
enforce(data.username).isNotBlank();
});
});
suite.focus({ onlyGroup: 'account' }); // typed group name
```
:::note Focused runs
When you focus the suite with `suite.only()`, `suite.skip()`, or `suite.focus()`, Vest intelligently subsets your validation schema under the hood using `enforce.pick` and `enforce.omit`. This ensures that schema validation still runs securely for the fields in focus—and provides correct types in the test callback!—while safely ignoring un-focused fields and allowing you to validate partial payloads effectively.
```javascript
// Validate only the username field, enforcing the schema for 'username' while ignoring 'age'
suite.only('username').run({
username: 'example',
});
```
:::
## Schema Types
- `enforce.shape({})`: Strict shape. No extra keys allowed.
- `enforce.loose({})`: Loose shape. Extra keys are ignored.
- `enforce.partial({})`: Partial shape. All keys are optional, but if present must match the type. No extra keys.
- `enforce.isArrayOf(rule)`: Validates an array where every item matches the rule.
## Inspecting schema results
The suite result includes typed properties for accessing validated and parsed data:
- `result.value` — The parsed output when the suite is valid. Typed as the schema's output type. `undefined` when invalid.
- `result.types.input` — Carries the schema's input type for static analysis. At runtime, holds the parsed output value.
- `result.types.output` — Carries the schema's output type. At runtime, holds the parsed output value.
- `result.run.data.raw` — The current run data passed into the suite callback (parsed when schema validation succeeds; original input when it fails).
- `result.run.data.parsed` — Parsed data for the current run.
```typescript
const schema = enforce.shape({
score: enforce.isNumeric().toNumber(),
});
const suite = create(data => {
test('score', () => {
enforce(data.score).greaterThan(0);
});
}, schema);
const result = suite.run({ score: '42' });
result.value; // { score: 42 }
result.types?.output; // { score: 42 }
result.run.data.raw; // { score: 42 }
result.run.data.parsed; // { score: 42 }
```
## Schema Parsing
Schema rules support built-in [data parsers](https://vestjs.dev/docs/enforce/builtin-enforce-plugins/data_parsers) that transform values as part of validation. When a schema uses parsers, `suite.run()` receives the transformed data in the callback, and `result.value` contains the parsed output.
```js
import { create, test, enforce } from 'vest';
const schema = enforce.shape({
name: enforce.isString().trim().toTitle(),
age: enforce.isNumeric().toNumber().clamp(0, 120),
});
const suite = create(data => {
// data is already parsed: { name: 'Jane Doe', age: 120 }
test('name', 'Name is required', () => {
enforce(data.name).isNotBlank();
});
}, schema);
const result = suite.run({ name: ' jANE DOE ', age: '180' });
// result.value → { name: 'Jane Doe', age: 120 }
```
# The Suite Object
In the [Getting Started](https://vestjs.dev/docs/get_started) guide, we saw that `create()` returns a **Suite Object**. This object is your main interface for interacting with validations. It holds the state, runs the tests, and gives you the results.
import SuiteMethodsSandpack from '@site/src/components/Sandpack/SuiteMethods';
## Running Validations
The most common method you'll use is `.run()`. It executes the callback you passed to `create`.
```javascript
const result = suite.run(formData, 'username');
```
:::note Async & Promises
If your suite contains async tests (like checking a database), `.run()` returns a **Result Object** that also behaves like a **Promise**.
You can `await suite.run(data)` to wait for all async operations to finish.
Check out the **[Async Tests](https://vestjs.dev/docs/writing_tests/async_tests)** guide for details.
:::
## Passing Arguments
You aren't limited to just passing `data`. Whatever arguments you pass to `.run()` are forwarded directly to your suite callback. This is great for handling "modes" or "steps" in multi-step forms.
```javascript
const suite = create((data, currentStep) => {
if (currentStep === 'billing') {
// only validate billing fields
}
});
// Pass the step name as the second argument
suite.run(formData, 'billing');
```
## Stateless Validation (Server-Side)
If you are running Vest on the server (Node.js, Deno, etc.), you usually don't want the suite to "remember" the previous request. You want a fresh start for every API call.
Use `.runStatic()` for this. It runs the suite, returns the result, and immediately discards the state.
```javascript
// Perfect for API handlers
app.post('/register', (req, res) => {
const result = suite.runStatic(req.body);
if (result.hasErrors()) {
return res.status(400).json(result.getErrors());
}
});
```
## Managing State
Vest is "stateful" by default. This means if you run validation for just _one_ field, Vest remembers the results of the _other_ fields from the previous run. This is excellent for Single Page Applications (SPAs) where you don't want to lose existing errors just because the user updated a different input.
However, sometimes you need to intervene manually.
### Resetting the Suite
If a user clears the form or navigates away, you might want to wipe the slate clean.
```javascript
// Clears all results, errors, and warnings
suite.reset();
```
### Resetting a Single Field
To clear errors for just one field (useful if you want to implement a "reset input" button):
```javascript
suite.resetField('email');
```
### Removing a Field
If a field is dynamically removed from your UI (like removing a row in a list), you should remove it from the validation state so `isValid()` doesn't get stuck waiting for it.
```javascript
suite.remove('passenger_2');
```
## Accessing Results Without Running
Sometimes you need to check the validity of the form _without_ triggering a new run (e.g., to disable a submit button).
```javascript
const result = suite.get();
```
This returns the most recent result object instantly.
## Advanced: SSR & Hydration
If you are rendering on the server and hydrating on the client (Next.js, Remix, Nuxt), you can transfer Vest's state so the client picks up exactly where the server left off.
We use the `SuiteSerializer` API for this.
```javascript
import { SuiteSerializer } from 'vest/exports/SuiteSerializer';
// 1. Server: Serialize the suite after running
const result = suite.runStatic(formData);
const serializedState = SuiteSerializer.serialize(result);
// 2. Client: Resume the suite with that state
SuiteSerializer.resume(suite, serializedState);
```
For a deep dive into this pattern, read **[Server-Side Rendering & Hydration](https://vestjs.dev/docs/suite_serialization)**.