Vest Documentation

repository·latest·Indexed 25 days ago

https://github.com/ealush/vest

A stateful validation library that allows writing validation rules using a syntax similar to unit tests. Vest is designed for progressive, incremental validation in complex forms, supporting asynchronous checks, focused runs via suite.only() and suite.focus(), and integration with React Hook Form and Zod. It features a chainable API via enforce(), support for dynamic lists with each(), and Standard Schema V1 interoperability.

Tokens
82K
Snippets
234
Records
437
Agent score
83%

What's inside Vest

  1. Key improvements in Vest@5

    latest

    Vest@5 introduces several changes to performance, usability, and type safety:

    • Performance: By default, Vest@5 stops after the first failure of each field to reduce overhead.
    • Suite Methods: Suite methods are now directly available on the suite object, reducing the need for suite.get() calls.
    • Error Handling: The suite result provides an ordered list of all failures, allowing for easier identification of the first error.
    • Simplified Error Retrieval: New singular getError and getWarning methods are available, eliminating the need to manually index into arrays.
    • TypeScript Support: Enhanced strict typing for field names across the suite.
  2. Understand the Isolate Architecture in @vestjs-runtime

    latest

    In @vestjs-runtime, an Isolate is the fundamental building block of the state and execution tree. Every piece of business logic or structural grouping—such as a test, a suite, a group, or a focus context—is represented as an Isolate. Isolates create isolated contexts to prevent sibling states from polluting one another during execution.

    Every Isolate contains:

    • Type: A string defining the nature of the Isolate (e.g., 'Test', 'Suite', 'Group').
    • Parent / Children: Links used to construct the state tree structure.
    • Data: Metadata required for execution.
    • Status: The current resolution state (e.g., PENDING, DONE).
  3. Understand the Vest Repository Package Boundaries

    latest

    The repository is structured into specific packages with strict dependency rules to prevent circular dependencies:

    PackageDescriptionAllowed Dependencies
    vest-utilsLow-level shared utilities (Types, FP helpers)NONE
    contextExecution context systemvest-utils
    vastStandalone state container utilityvest-utils
    anyoneCompound boolean predicate utilityvest-utils
    vestjs-runtimeState management engine (Isolates, Bus, Reconciler)vest-utils, context
    n4s"Enforce" assertion libraryvest-utils, context
    vestMain validation library (Public API, Suites)n4s, vestjs-runtime, vest-utils, context
    vxInternal CLI and build toolingNode.js native only
  4. Understand n4s schema parsing and transformation

    latest
    The n4s schema architecture uses a single-pass pipeline to perform both validation and transformation. When a schema is executed, it validates the input container, rejects dangerous keys (__proto__, prototype, constructor), iterates over schema keys, and runs field rules to collect transformed outputs. This ensures that validation and data coercion (e.g., converting a string to a number) happen in one pass.
  5. Handle asynchronous warnings with useWarn()

    latest

    If the severity of a warning depends on an asynchronous operation, you must capture the setter returned by useWarn() synchronously before the first await. This ensures the setter is bound to the active test context. You can then call the captured setter later once the async work completes.

    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();
    });
  6. Integrate Vest with Schema Validators like Zod or Enforce

    latest

    Vest can be used alongside schema validators to combine structural parsing with stateful validation workflows.

    • Schema Validators (Zod, Enforce, Valibot): Best for one-shot structural validation, data transformation, and parsing unknown input at boundaries.
    • Vest Suites: Best for managing validation over time, handling async checks, coordinating dependent fields, and providing progressive UX (warnings, pending states).

    You can attach a schema to a Vest suite so that the parsed, transformed values are used during the suite's execution.

    const accountSchema = enforce.shape({
      age: enforce.isNumeric().toNumber(),
      email: enforce.isString().trim(),
    });
    
    // Attach the schema to the suite to use parsed values
    const accountSuite = create(parsedAccount => {
      test('age', 'Must be an adult', () => {
        enforce(parsedAccount.age).greaterThanOrEquals(18);
      });
    }, accountSchema);
    
    // Run the suite with input
    const result = accountSuite.runStatic(input);
    if (result.isValid()) persist(result.value);
  7. Validate multi-step forms using groups

    latest

    To handle multi-step forms (wizards), model each step as a Vest group. This allows you to validate the current step independently while maintaining the state of previously completed steps.

    When a user attempts to move to the next step, use .focus({ onlyGroup: stepName }) to run validation only for that specific group. This ensures that errors in future steps do not block progress in the current step.

    Key considerations:

    • Step Navigation: Use result.isValidByGroup(stepName) to determine if a user can proceed.
    • Final Submission: Always run the full suite using .run(data) at the end of the workflow to ensure the entire form is valid.
    • Top-level tests: The onlyGroup option excludes ungrouped top-level tests. Ensure all step-specific requirements are contained within their respective groups.
    import { create, enforce, group, test } from 'vest';
    
    type Step = 'account' | 'profile' | 'billing';
    
    type OnboardingData = {
      displayName: string;
      email: string;
      plan: string;
    };
    
    // 1. Define the suite with groups representing steps
    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();
        });
      });
    });
    
    // 2. Validate only the current step for navigation
    async function canContinue(step: Step, data: OnboardingData) {
      const result = await onboardingSuite.focus({ onlyGroup: step }).run(data);
      return result.isValidByGroup(step);
    }
    
    // 3. Validate the entire suite for final submission
    async function submitForm(data: OnboardingData) {
      const result = await onboardingSuite.run(data);
    
      if (!result.isValid()) {
        // Handle errors (e.g., navigate to first invalid step)
        return;
      }
      // Proceed with submission
    }
  8. Specify custom failure messages with the message modifier

    latest

    When using enforce, you can provide custom error messages that are thrown when a validation rule fails. Use the .message() modifier immediately before the rule it is intended to describe. If a message is provided, it overrides the default message for all subsequent rules in the chain.

    enforce(value)
      .message('Value must be a number')
      .isNumber();
    
    // Note: In a single chain, the message applies to the rule following it
    enforce(value)
      .message('Value must be positive')
      .isPositive();
  9. Automated Migration Prompt for V5 to V6

    latest

    If you are migrating a large codebase, you can use the following prompt with an LLM to automate the refactoring of Vest 5 suites to Vest 6:

    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.
  10. Use Vest with Form State Managers

    latest

    Vest is decoupled from form management. It does not own input registration, field values, or submission mechanics. This allows you to compose it with libraries like React Hook Form (RHF), Formik, or Vue/Svelte composables.

    Recommended Pattern:

    • Form Manager: Owns input mechanics (values, registration, events, submission).
    • Vest: Owns progressive validation behavior (stateful checks, async coordination, warnings).
  11. Compose Enforce Rules for reuse

    latest

    You can combine multiple enforce rules into a single reusable validator using the compose function. This is useful when a specific set of rules describes a single logical concept (e.g., a valid age or a valid email) and you want to avoid repeating those checks across different schemas.

    Composed rules behave identically to standard enforce rules: they can be used for standalone assertions via .run(), as type guards, or within an enforce.shape() schema.

    import { enforce, compose } from 'vest';
    
    const isValidAge = compose(
      enforce.isNumber(),
      enforce.greaterThanOrEquals(18),
      enforce.lessThan(120),
    );
    
    // Usage as a standalone validator
    isValidAge.run(20); // { pass: true }
    isValidAge.run(15); // { pass: false }
    
    // Usage inside a schema
    const userSchema = enforce.shape({
      age: isValidAge,
    });