validator-flow
v1.0.0
Published
Composable sync and async validation handlers with middleware and validator groups for JavaScript
Downloads
194
Maintainers
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/falseinstead 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 MigrationThree 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 --> GroupResultData 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
- API overview
- Use cases
- AI agent setup
- Contributing
- Contact
- License
📦 Install
npm install validator-flowconst {
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
onlyorexcept - Use
createSyncValidatororcreateAsyncValidatorwithwithFieldFilter/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)wherefn(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 concurrentlyFull 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']orexcept: ['profile']togroup.validate()
Example: validate only billing
const result = await group.validate({ only: ['billing'] });
// profile and shipping are skipped; their results are not re-runFull 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 dirtyvalidate({ 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 resultsFull 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
createValidatorGroupalways 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-flowInstall agent skill (optional, fetches from GitHub)
npx validator-flow-skill --cursor
npx validator-flow-skill --claude-code
npx validator-flow-skill --allOne-shot without a prior install:
npx -p validator-flow validator-flow-skill --cursorRequirements: 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-marketplaceLocal development:
claude --plugin-dir ./claude-pluginSee claude-plugin/README.md for details.
Note:
npm installalone does not load agent skills. Runnpx validator-flow-skillor 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:
- Context — the real scenario you faced (what you were building, what broke or felt wrong)
- What you tried — existing APIs, workarounds, or gaps you ran into
- 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
- LinkedIn: Manish Sharma
📄 License
ISC
