npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

validator-flow

v1.0.0

Published

Composable sync and async validation handlers with middleware and validator groups for JavaScript

Downloads

194

Readme

ValidatorFlow ✨

A structured way to handle validation in JavaScript — sync, async, legacy-friendly, and built to scale with your project.


🎯 Purpose and Core Idea

This library provides a managed approach to validation across your project. Whether you are building a new sign-up form or maintaining a large legacy codebase, validator-flow gives you a consistent pattern to define, run, and orchestrate validation logic.

The problem it solves

In legacy projects, validation is often scattered and unmanaged:

  • Old built-in functions return true / false instead of a standard shape
  • Some validators throw errors; others return strings or nothing at all
  • The same rules are copy-pasted across files with no shared structure
  • Migrating to a new validation library usually means rewriting everything at once

Most libraries solve "how to validate" — but not "how to bring your existing validation along for the ride."

What makes this different

validator-flow standardizes on one contract:

// Every validator returns this shape
{ isValid: boolean, error: string }

You can wrap legacy validators as-is, use them inside a clean config, and gradually adopt middleware, async checks, and multi-section groups — without a big-bang rewrite.

Mental model

| Layer | API | What it does | |-------|-----|--------------| | Handler | SyncValidatorHandler / AsyncValidatorHandler | Validates one form/object from state + config | | Factory | createSyncValidator / createAsyncValidator | Same as a handler, plus .use() middleware per field | | Group | createValidatorGroup | Orchestrates multiple named sections (profile, billing, shipping) into one consolidated result |


How it works

ValidatorFlow is a validation runtime — it does not ship validation rules. You bring your own validators; the library standardizes how they run and how results are combined.

                         ValidatorFlow
                              │
        ┌─────────────────────┼─────────────────────┐
        ↓                     ↓                     ↓
  Standardization        Orchestration          Middleware
        │                     │                     │
  { isValid, error }   groups / sections      field-level .use()
        │                     │                     │
        └─────────────────────┼─────────────────────┘
                              ↓
                    Sync + Async Validation
                              │
                              ↓
                     Legacy Integration
                              │
                              ↓
                   Incremental Migration

Three pillars at the top — one contract, multi-section flows, cross-cutting hooks — all feeding into sync/async handlers that wrap legacy code so you can migrate gradually, not all at once.

Technical layers

flowchart TB
  subgraph validators [Your validators]
    Legacy["Legacy code\nboolean / throw / strings"]
    Rules["Field rules\n{ isValid, error }"]
  end

  subgraph handlers [Layer 1 — Handlers]
    Sync["SyncValidatorHandler\none form, sync fields"]
    Async["AsyncValidatorHandler\none form, async fields"]
  end

  subgraph factory [Layer 2 — Factory + middleware]
    CreateSync["createSyncValidator"]
    CreateAsync["createAsyncValidator"]
    Middleware[".use chain\nwithFieldFilter / withAsyncFieldFilter"]
  end

  subgraph group [Layer 3 — ValidatorGroup]
    VG["createValidatorGroup\nprofile · billing · shipping"]
    Options["only / except / onlyDirty\nparallel · stopOnFirstInvalid"]
  end

  subgraph result [Output]
    FieldResult["{ isValid, errors }"]
    GroupResult["{ isValid, results, skipped }"]
  end

  Legacy -->|"wrap adapters"| Rules
  Rules --> Sync
  Rules --> Async
  Sync --> CreateSync
  Async --> CreateAsync
  CreateSync --> Middleware
  CreateAsync --> Middleware
  Sync --> VG
  Async --> VG
  CreateSync --> VG
  CreateAsync --> VG
  Middleware --> FieldResult
  Sync --> FieldResult
  Async --> FieldResult
  VG --> Options
  Options --> GroupResult

Data flow in one pass:

state + config  →  Handler / Factory  →  { isValid, errors: { field: message } }
                         ↓
              ValidatorGroup (optional)  →  { isValid, results: { section: ... } }

Pick the smallest layer that fits: a handler for one flat form, a factory when you need middleware, a group when you have multiple sections or wizard steps.


Table of contents


📦 Install

npm install validator-flow
const {
  SyncValidatorHandler,
  AsyncValidatorHandler,
  createSyncValidator,
  createAsyncValidator,
  createValidatorGroup,
  withFieldFilter,
  withAsyncFieldFilter,
} = require('validator-flow');

📚 API Overview

| Export | When to use | |--------|-------------| | SyncValidatorHandler | One-shot sync validation, no middleware | | createSyncValidator | Sync validation with a field-level middleware chain | | AsyncValidatorHandler | One-shot async validation (API checks, DB lookups) | | createAsyncValidator | Async validation with middleware | | createValidatorGroup | Multiple form sections, wizard steps, or domain areas | | withFieldFilter | Run sync middleware only on selected fields (only / except) | | withAsyncFieldFilter | Same as above, for async middleware |


💡 Use Cases

Each section below describes a real-world scenario, when to reach for it, a short code example, and a link to a runnable file.


⚡ Core Handlers

Sync form validation

Real-world scenario: You are building a sign-up form with name, email, and password. All rules are synchronous — required checks, min length, format — and you want one function call that returns all field errors at once.

When to use it:

  • ✅ Simple forms with in-process rules
  • ✅ No middleware or cross-field async checks needed
  • ✅ You want the lightest API surface

Example: validate a sign-up form

const { SyncValidatorHandler } = require('validator-flow');

const config = {
  name:    { validate: (value) => required(value) },
  email:   { validate: (value) => email(value) },
  password:{ validate: (value) => minLength(6)(value) },
};

const state = { name: 'Jane', email: '[email protected]', password: 'secret' };
const result = SyncValidatorHandler(state, config);
// { isValid: true, errors: { name: '', email: '', password: '' } }

Full example: examples/sync-basic.js


Async validation

Real-world scenario: Before a user submits registration, you need to check whether their username is already taken and whether their email domain is allowed — both require simulated or real API calls.

When to use it:

  • ✅ Validators return a Promise<{ isValid, error }>
  • ✅ Network, database, or file-system checks
  • ✅ You want sequential or parallel async execution via helperConfig.strategy

Example: async username and email checks

const { AsyncValidatorHandler } = require('validator-flow');

const config = {
  username: {
    validate: async (value) => {
      const r = await requiredAsync(value);
      if (!r.isValid) return r;
      return checkUsernameAvailable(value); // simulated API
    },
  },
  email: { validate: (value) => checkEmailDomain(value) },
};

const result = await AsyncValidatorHandler(state, config, { strategy: 'parallel' });
// { isValid: boolean, errors: { username: '', email: '' } }

Full example: examples/async-basic.js


🔧 Middleware and Field Filters

Field-level middleware

Real-world scenario: You want to log, trim, or audit validation — but only for sensitive fields like email and password, not every field on the form.

When to use it:

  • Cross-cutting concerns around validation (logging, metrics, trimming)
  • Middleware should run for a subset of fields via only or except
  • Use createSyncValidator or createAsyncValidator with withFieldFilter / withAsyncFieldFilter

Example: middleware runs only for email

const { createSyncValidator, withFieldFilter } = require('validator-flow');

const logWhenRun = withFieldFilter((ctx, next) => {
  console.log(`Validating field: ${ctx.fieldName}`);
  next();
});

const validator = createSyncValidator()
  .use(logWhenRun, { only: ['email'] });

const result = validator.validate(state, config);

Full example: examples/sync-field-filter.js


🏛️ Legacy Migration

Wrap legacy validators

Real-world scenario: Your project has years of validation helpers — isNotEmpty() returns a boolean, assertMinLength() throws on failure — used inconsistently across dozens of files. You cannot afford to rewrite them all, but you want a managed, unified validation flow going forward.

When to use it:

  • ✅ Existing validators do not return { isValid, error }
  • ✅ You need a thin adapter layer, not a full migration
  • ✅ This is the core differentiator — reuse old code as-is

Example: adapt boolean and throwing validators

const { SyncValidatorHandler } = require('validator-flow');

// Legacy: returns true/false
function isNotEmpty(value) {
  return value != null && String(value).trim() !== '';
}

// Adapter: boolean → { isValid, error }
function fromBoolean(legacyFn, errorMessage) {
  return (value) => {
    const valid = legacyFn(value);
    return { isValid: !!valid, error: valid ? '' : errorMessage };
  };
}

const config = {
  name: { validate: fromBoolean(isNotEmpty, 'Name is required') },
};

const result = SyncValidatorHandler({ name: '' }, config);
// { isValid: false, errors: { name: 'Name is required' } }

Full example: examples/legacy-validator-wrap.js


🧩 Validator Groups

Multi-section forms

Real-world scenario: A checkout page has separate profile and billing sections, each with its own validator and state. On submit, you need one consolidated pass/fail result across all sections.

When to use it:

  • Multiple independent form areas (profile, billing, shipping)
  • Each section has its own state, config, and validator
  • You want a single validate() that returns per-section and overall results

Example: profile + billing group

const { createValidatorGroup, SyncValidatorHandler } = require('validator-flow');

const group = createValidatorGroup({
  profile: {
    validator: SyncValidatorHandler,
    state: { name: 'Jane', email: '[email protected]' },
    config: profileConfig,
  },
  billing: {
    validator: SyncValidatorHandler,
    state: { card: '4111111111111111', expiry: '12/99' },
    config: billingConfig,
  },
});

const result = await group.validate();
// { isValid, results: { profile: {...}, billing: {...} }, skipped: [] }

Full example: examples/validator-group-basic.js


Group middleware

Real-world scenario: You want to log or time how long each section (profile, billing) takes to validate — useful for debugging slow forms or adding observability in production.

When to use it:

  • Cross-cutting logic at the section level, not individual fields
  • Register middleware with group.use(fn) where fn(groupCtx, next) wraps each section

Example: log each section as it validates

const group = createValidatorGroup(definitions);

group.use(async (groupCtx, next) => {
  console.log(`Validating section: ${groupCtx.name}`);
  await next();
  console.log(`${groupCtx.name} -> ${groupCtx.result.isValid ? 'ok' : 'invalid'}`);
});

const result = await group.validate();

Full example: examples/validator-group-middleware.js


Execution strategies

Real-world scenario: Your form has three async sections (profile, billing, shipping), each hitting an API. Running them in parallel saves time; stopping at the first invalid section saves unnecessary API calls.

When to use it:

  • strategy: 'default' — sequential (respects order)
  • strategy: 'parallel' — all sections at once (faster for async)
  • strategy: 'stopOnFirstInvalid' — stop as soon as one section fails

Example: parallel async validation

const group = createValidatorGroup(definitions);

const result = await group.validate({ strategy: 'parallel' });
// All async sections run concurrently

Full example: examples/validator-group-strategies.js


Validate a subset of sections

Real-world scenario: A multi-step wizard is on step 2 (billing). You only want to validate the billing section right now — profile was already validated on step 1.

When to use it:

  • Wizard / step-by-step flows
  • Partial submit or "validate this tab only"
  • Pass only: ['billing'] or except: ['profile'] to group.validate()

Example: validate only billing

const result = await group.validate({ only: ['billing'] });
// profile and shipping are skipped; their results are not re-run

Full example: examples/validator-group-only-except.js


Dirty-only revalidation

Real-world scenario: A large form has three async sections, each taking ~150ms. The user edits only the profile field — you should not re-run billing and shipping checks that already passed.

When to use it:

  • Performance-sensitive forms with expensive async validators
  • group.update(name, partial) marks a section dirty
  • validate({ onlyDirty: true }) re-runs only changed sections; others use cache

Example: re-validate only what changed

await group.validate(); // first run — all sections execute

group.update('profile', { state: { name: '', email: 'bad' } });

const result = await group.validate({ onlyDirty: true });
// only profile re-runs; billing and shipping use cached results

Full example: examples/validator-group-onlyDirty.js


Mixed sync and async sections

Real-world scenario: Profile validation is simple and synchronous; payment validation requires an async API call to verify the card. One validator group should handle both without forcing everything to be async.

When to use it:

  • Different sections have different validation types
  • createValidatorGroup always returns a Promise; sync validators are wrapped automatically

Example: sync profile + async payment in one group

const group = createValidatorGroup({
  profile: {
    validator: SyncValidatorHandler,
    state: profileState,
    config: profileConfig,
  },
  payment: {
    validator: AsyncValidatorHandler,
    state: paymentState,
    config: paymentConfig,
  },
});

const result = await group.validate();

Full example: examples/validator-group-mixed-sync-async.js


Dynamic sections

Real-world scenario: Your app starts with profile + billing validation, but after the user selects "pickup" instead of "delivery", you need to swap to a shipping-only validation setup at runtime.

When to use it:

  • Section definitions change based on user choices or app state
  • group.setDefinitions(newDefinitions) replaces all sections, clears cache, and marks everything dirty

Example: swap section definitions at runtime

const group = createValidatorGroup(initialDefinitions);
await group.validate();

// User changes flow — replace with shipping-only setup
group.setDefinitions(shippingOnlyDefinitions);
const result = await group.validate();

Full example: examples/validator-group-setDefinitions.js


🚀 Run examples locally

Clone the repo and run any example directly:

node examples/sync-basic.js
node examples/legacy-validator-wrap.js
node examples/validator-group-onlyDirty.js

🤖 AI agent setup (Cursor & Claude Code)

Agent skills are not bundled in the npm package (keeps node_modules lean). Install them on demand — the CLI fetches files from the GitHub repository at the matching version tag.

Skill content (in the GitHub repo):

| File | Purpose | |------|---------| | SKILL.md | Decision tree, workflow, anti-patterns | | reference.md | API and options | | patterns.md | Legacy adapters and recipes |

Install library

npm install validator-flow

Install agent skill (optional, fetches from GitHub)

npx validator-flow-skill --cursor
npx validator-flow-skill --claude-code
npx validator-flow-skill --all

One-shot without a prior install:

npx -p validator-flow validator-flow-skill --cursor

Requirements: network access; GitHub repo must have git tag v{version} matching the npm version (e.g. v1.0.0 for 1.0.0).

Developing this library from source

Copy skills locally without fetching from GitHub:

cp -r skills/validator-flow .cursor/skills/

Claude Code — plugin (auto-discovered skill, optional)

Install the plugin from this repository for automatic skill discovery in Claude Code:

/plugin marketplace add https://github.com/manishsharma130/validator-flow
/plugin install validator-flow@validator-flow-marketplace

Local development:

claude --plugin-dir ./claude-plugin

See claude-plugin/README.md for details.

Note: npm install alone does not load agent skills. Run npx validator-flow-skill or use the Claude Code plugin.


🤝 Contributing

We welcome contributions from the community. Whether you are improving docs or evolving the core design, here is how to help.

📝 Documentation improvements

If you feel this README can be clearer, an example is missing, or a use case needs a better explanation — please open a PR. We are happy to review and merge documentation improvements.

🧠 Logic and core idea improvements

If you hit a real-world edge case, notice a bug, or believe the core design should evolve — please open a PR that includes:

  1. Context — the real scenario you faced (what you were building, what broke or felt wrong)
  2. What you tried — existing APIs, workarounds, or gaps you ran into
  3. Your proposal — how the change improves or thoughtfully extends the library's vision

We value PRs that explain the why, not just the what.


Contact


📄 License

ISC