# Vest: a validation state layer that runs like a unit-test suite

> Vest is a TypeScript validation framework that executes only the rules for the field or step that changed, keeps earlier results, and discards stale async completions. It is not a form state manager and not a payload parser, and the README says so.

**ealush/vest** — Vest ✅ Declarative validations framework

- Repository: https://github.com/ealush/vest
- Website: https://vestjs.dev/
- Stars: 2,663 · Forks: 94
- Language: TypeScript
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/ealush-vest

## The problem Vest targets: validation state, not form state

Most validation code is written for the whole form. You collect every field, run every rule, and get a flat list of errors back. That works until a single keystroke in an email field triggers a network request for a username that has not changed. Vest's answer is to treat validation as a stateful runtime: rules are declared once, but a run can be scoped to a field, a group, or a step, and the results of everything else are retained from the previous run.

The README places this in a table of layers. Form state (values, registration, touched state, submission) belongs to tools like React Hook Form or Formik. The data boundary (parsing and protecting a complete payload) belongs to Zod, Valibot, Ajv, or Enforce schemas. Vest claims the middle layer: deciding what runs now, retaining results, and coordinating async work. The project states these layers are complementary and describes a common architecture that uses all three at once.

That positioning is the useful part. Vest is not competing for the job of holding your input values, and it does not try to be the thing that rejects a malformed HTTP body. It is for teams whose validation is expensive, order-dependent, or asynchronous, and who are tired of re-running all of it on every change.

## How the suite and the runtime work together

A suite is created with create(), which takes a callback. Inside that callback you call test(fieldName, message, callback). The first two arguments bind the assertion to a field and to the message shown when it fails. The third argument is the assertion itself, and it may be async. The README's example declares two synchronous username checks and one async availability check that receives a signal object and passes it to the network call.

Execution is where the design shows. Calling suite.only('username').run(formData) runs only the username tests and retains previous results for every other field. The returned result object exposes per-field queries such as isPending('username') and hasErrors('username'), and the result itself is awaitable when you need the current async run to finish. The README summarises the runtime in one line: Vest validates what changed, remembers what already passed, and prevents stale async validation results.

The race handling is the part that is hard to reproduce by hand. The README states that when async checks overlap, only the latest result can update the suite, and that obsolete requests can be cancelled and stale completions ignored. The mechanism visible in the example is the signal passed into the async test callback, which is what a fetch-style API would use to abort. Vest is framework-independent: the README lists React, Vue, Svelte, Angular, vanilla JavaScript, and Node.js as targets for the same suite, and notes that a suite can run statelessly on the server and resume full validation state in the browser.

## Installing Vest and running a scoped suite

The README gives one install command. Run it in your project root; it pulls the vest package from npm, which is the same package name used in the import.

```bash
npm i vest
```

After installation, create a suite file outside your components. The README's example imports create, enforce, and test from vest, then declares three tests against the same username field: a blank check, a length check, and an async availability check that receives a signal.

```js
import { create, enforce, test } from 'vest';

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('username', 'Username is already taken', async ({ signal }) => {
    const response = await checkUsername(data.username, { signal });
    enforce(response.available).isTruthy();
  });
});
```

Running the suite against a changed field is a two-step call: scope it with only(), then run it with the current form data. The result exposes per-field queries, and awaiting the result waits for the in-flight async check.

```js
const result = suite.only('username').run(formData);

result.isPending('username');
result.hasErrors('username');

await result;
```

What you should see: isPending('username') is true while the async check is in flight, hasErrors('username') reflects the synchronous checks that already ran, and the await resolves once the latest async run settles. Every other field keeps the result it had before this run.

## Where Vest is the wrong tool

The README has a section titled "Where Vest works well" and, immediately after it, a sentence that rules out a whole class of use: for a trivial synchronous form or a one-shot API parse, native HTML validation or a schema validator may be all you need. That is an unusually direct limitation for a project to publish about itself, and it should be taken at face value.

The reason is structural. Vest's value comes from retained state and scoped execution. If your form has four fields, no async checks, and no cross-field rules, there is nothing to retain and nothing to scope, so you pay for a runtime without using it. Likewise, if you only need to reject a malformed JSON body at an API boundary, a schema validator expresses that in one declaration and gives you a parsed, typed value. Vest's result object tells you which tests failed; it is not a parser, and the README does not present it as one.

The async model also imposes a requirement on your own code. The example passes the signal from the test callback into the network call. The README states that Vest can cancel obsolete requests and ignore stale completions, but that behaviour depends on the request actually accepting the signal. An async check that ignores it and resolves on its own schedule is outside what the documented mechanism controls. The README does not document rollback of retained state, so if you need to discard a previous result entirely rather than update part of it, that path is not described.

## Vest against a schema validator: different jobs, different shapes

The obvious alternative for a TypeScript team is a schema validator such as Zod or Valibot, and the README names both alongside Ajv and Vest's own Enforce schemas in its layer table. The difference is not quality, it is the unit of work.

A schema validator describes the shape of a complete value and returns either a parsed value or a list of issues. It is stateless by design: you hand it a payload, it hands back a verdict. That makes it a good fit for the server boundary, where you have the whole object and want a parsed result. It has no concept of a field being pending, no memory of a previous run, and no way to run one property's rules without touching the rest.

Vest inverts that. It is stateful and incremental, built around the question of what changed since the last run. The README's table puts the two in different rows on purpose, and the project's own guidance is to use both: a form manager for input mechanics, Vest for progressive interaction, and a schema validator for the final submitted boundary. If you already have Zod at the boundary, adding Vest does not replace it. If you were hoping Vest would also parse and type your payload, the README points you to Enforce schemas and Standard Schema interoperability instead, while keeping run() and runStatic() as the execution APIs.

## Maintenance, licence, and what a version bump costs

The repository is not archived, and the last push to the latest branch was on 2026-09-24. The most recent releases listed are vestjs-runtime@2.0.17, vest@6.3.2, and vest-utils@2.0.17, all published on 2026-04-07. The gap between the April releases and the September push is worth noting if you are planning around release cadence: the code is moving, but the published version you would install has been stable for several months.

The licence is MIT, which permits commercial use and modification. That is a permissive licence, but it is not legal advice, and if your organisation has a policy on dependency licences you should route the LICENSE file through whoever owns that policy.

The upgrade surface is larger than a single package. The repository is a monorepo with a packages/ directory and the vestjs-runtime and vest-utils packages published separately, so a version bump can move more than one artefact. The root package.json defines build, test, and release through a vx task runner, and the test script runs vitest with typecheck enabled. If you fork or vendor Vest, expect to inherit that toolchain rather than a plain npm package. The README also links a public roadmap on the latest branch, which is where planned changes are described; the README itself does not document a deprecation policy or a rollback procedure.

## Conclusion

Adopt Vest if your forms have async checks, dependent fields, optional sections, or a browser and server that must share the same rules, and if you are willing to keep a form manager such as React Hook Form for input mechanics. Skip it for a trivial synchronous form or a one-shot payload parse, where the README points to native HTML validation or a schema validator. Before committing, verify three things against your own code: that your bundler resolves the ESM entry of vest, that your async checks honour the signal passed into the test callback, and that your submitted payload still passes a schema validator at the server boundary, because Vest's result object is not a parser.

## FAQ

### What does Vest do?

Vest is a declarative validation framework in which rules are written like unit tests. It runs the tests for the field or step that changed, keeps the results for everything else, and prevents stale async validation results from updating the suite.

### How do I install Vest?

The README gives a single command, npm i vest, which installs the package from npm under the same name used in the import statement.

### Can Vest replace Zod or another schema validator?

No. The README puts them in different layers: schema validators parse and protect a complete payload at the data boundary, while Vest handles validation state, deciding what runs now and coordinating async work. The project describes a common architecture that uses both.

### How does Vest handle async validation race conditions?

The README states that when async checks overlap, only the latest result can update the suite. Async test callbacks receive a signal that the example passes to the network call, which is how obsolete requests are cancelled and stale completions ignored.

### Does Vest work with React, Vue, and Svelte?

The README lists React, Vue, Svelte, Angular, vanilla JavaScript, and Node.js as targets for the same suite, and describes the framework as framework-independent.

## Sources

- [ealush/vest on GitHub](https://github.com/ealush/vest)
- [License: MIT](https://github.com/ealush/vest/blob/latest/LICENSE)
- [Project website](https://vestjs.dev/)
- [README](https://github.com/ealush/vest/blob/latest/README.md)
- [Releases](https://github.com/ealush/vest/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ealush-vest
