v1.2 — Production Ready

Validate. Sanitize.
Chain.

Fluent, chainable validation for JavaScript & TypeScript.
Zero dependencies. Under 3kb gzipped. Works everywhere.

2.7kb gzipped
0 dependencies
134 tests passing
31M ops/sec
npm install chain-validate
example.ts
import { v } from 'chain-validate';

const schema = v.object({
  name:  v.string().required().trim().minLength(2),
  email: v.string().required().trim().lowercase().email(),
  age:   v.number().optional().coerce().min(0),
});

const result = schema.validate({
  name:  '  KENIL  ',
  email: '  Kenil@Example.COM  ',
  age:   '25'
});

// result.ok === true
// result.value = {
//   name: "KENIL",
//   email: "kenil@example.com",
//   age: 25
// }

Validate + Sanitize in One Chain

1
Define rules fluently

Chain validators and sanitizers in the order you want them to run.

2
Get clean data back

result.value is already trimmed, lowercased, and coerced. No post-processing needed.

3
Never catches exceptions

Check result.ok — TypeScript narrows the type automatically.

Why chain-validate?

Everything you need. Nothing you don't.

All Errors at Once

Never short-circuits. Every rule runs and all failures are collected — perfect for forms showing multiple field errors simultaneously.

Never Throws

Returns {ok, value/errors} — a discriminated union. No try/catch. TypeScript narrows automatically on .ok.

Built-in Sanitization

.trim(), .lowercase(), .coerce() transform the value as it flows — result.value is already clean.

2.7kb Gzipped

Zero dependencies. Tree-shakable. 5x smaller than Zod, 10x smaller than Joi. Your bundle will barely notice it.

Blazing Fast

31M ops/sec on simple validations. Zero allocations on valid input. Pre-compiled regex. No Proxy or getter traps.

Isomorphic

Same import, same API. Works in Node.js for API validation and in the browser for form validation. Define once, use everywhere.

How It Compares

Feature chain-validate Zod Yup Joi
Bundle size ~2.7kb ~14kb ~12kb ~30kb
Zero dependencies Yes Yes Yes No
Collects ALL errors Yes No No Config
Built-in sanitization Yes No Partial No
Never throws Yes No No No
TypeScript-first Yes Yes Partial No
Async validators Yes Yes Yes Yes
Conditional (.when) Yes No Yes Yes

Interactive Playground

Pick a scenario, edit the input, see results live.

schema.ts
input
result

API Reference

Every method, organized by chain type.

v.string()

StringChain

Validators

.required(msg?)Must not be empty/undefined/null
.optional()Allows undefined/null
.minLength(n, msg?)Minimum character count
.maxLength(n, msg?)Maximum character count
.length(n, msg?)Exact character count
.email(msg?)RFC 5322 email format
.url(msg?)Valid URL format
.uuid(msg?)UUID v4 format
.regex(pattern, msg?)Custom regex test
.includes(str, msg?)Must contain substring
.startsWith(str, msg?)Must start with prefix
.endsWith(str, msg?)Must end with suffix
.oneOf(values, msg?)Must be one of listed values
.notEmpty(msg?)Not empty after trim

Sanitizers

.trim()Strip whitespace
.lowercase()Convert to lowercase
.uppercase()Convert to uppercase
.replace(search, repl)String replacement
.default(value)Fallback if nil

v.number()

NumberChain

Validators

.required(msg?)Must not be undefined/null/NaN
.optional()Allows undefined/null
.min(n, msg?)Minimum value (inclusive)
.max(n, msg?)Maximum value (inclusive)
.between(min, max, msg?)Value within range
.integer(msg?)Must be whole number
.positive(msg?)Must be > 0
.negative(msg?)Must be < 0
.oneOf(values, msg?)Must be one of listed

Sanitizers

.coerce()"42" → 42
.round()Round to nearest int
.floor()Round down
.ceil()Round up
.clamp(min, max)Clamp within range
.default(value)Fallback if nil

v.boolean()

BooleanChain

Validators

.required(msg?)Must not be nil
.optional()Allows undefined/null

Sanitizers

.coerce()"true"/"yes"/1 → true
.default(value)Fallback if nil

v.array()

ArrayChain

Validators

.required(msg?)Must not be nil
.optional()Allows undefined/null
.of(chain)Validate each element
.minLength(n, msg?)Min array length
.maxLength(n, msg?)Max array length
.unique(msg?)All elements unique
.noEmpty(msg?)No empty elements

Sanitizers

.compact()Remove null/undefined
.flat(depth?)Flatten nested arrays
.default(value)Fallback if nil

v.object(schema)

ObjectChain

Validators

.required(msg?)Must not be nil
.optional()Allows undefined/null
.strict(msg?)Reject unknown keys

Features

nested pathsErrors include ["user","email"]
.when() fieldsConditional validation

Combinators

All Chains

Methods

.or(otherChain)Union — passes if either passes
.and(otherChain)Intersection — both must pass
.when(field, opts)Conditional based on sibling field
.test(name, msg, fn)Custom sync validator
.testAsync(name, msg, fn)Custom async validator

Custom Chain

v.custom(fn)Full control with ctx.addError()
v.any()Accepts any type

Real-World Examples

Copy-paste ready code for common scenarios.

Backend

Express API Validation

routes/users.ts
const createUserSchema = v.object({
  name:     v.string().required().trim().minLength(2),
  email:    v.string().required().trim().lowercase().email(),
  password: v.string().required().minLength(8),
  age:      v.number().optional().coerce().integer().between(13, 150),
  role:     v.string().default('user').oneOf(['user', 'admin']),
});

app.post('/users', (req, res) => {
  const result = createUserSchema.validate(req.body);

  if (!result.ok) {
    return res.status(400).json({
      success: false,
      errors: result.errors,
    });
  }

  // result.value is typed, trimmed, lowered, coerced
  const user = await createUser(result.value);
  res.json({ success: true, data: user });
});
Frontend

React Form Validation

LoginForm.tsx
const loginSchema = v.object({
  email:    v.string().required('Email is required')
               .email('Enter a valid email'),
  password: v.string().required('Password is required')
               .minLength(8, 'At least 8 characters'),
});

function LoginForm() {
  const [errors, setErrors] = useState({});

  const handleSubmit = (formData) => {
    const result = loginSchema.validate(formData);

    if (!result.ok) {
      const fieldErrors = {};
      result.errors.forEach((err) => {
        fieldErrors[err.path[0]] = err.message;
      });
      setErrors(fieldErrors);
      return;
    }

    setErrors({});
    login(result.value);
  };
}
Advanced

Conditional Validation

schema.ts
const schema = v.object({
  type: v.string().oneOf(['personal', 'business']),

  company: v.string().when('type', {
    is: 'business',
    then: (chain) => chain.required('Company required'),
    otherwise: (chain) => chain.optional(),
  }),

  taxId: v.string().when('type', {
    is: 'business',
    then: (chain) => chain.required().length(10),
    otherwise: (chain) => chain.optional(),
  }),
});

// Business: company + taxId required
// Personal: both optional
Custom

Password Strength Validator

validators.ts
const password = v.custom((value, ctx) => {
  if (typeof value !== 'string') {
    ctx.addError('type', 'Must be a string');
    return;
  }

  if (value.length < 8)
    ctx.addError('minLength', 'At least 8 characters');

  if (!/[A-Z]/.test(value))
    ctx.addError('uppercase', 'Need one uppercase');

  if (!/[0-9]/.test(value))
    ctx.addError('digit', 'Need one digit');

  if (!/[^a-zA-Z0-9]/.test(value))
    ctx.addError('special', 'Need one special char');

  return value;
});

// password.validate("abc")
// errors: minLength, uppercase, digit, special
// All 4 errors at once!

Result Shape

The discriminated union that powers everything.

OK
{
  ok: true,
  value: "kenil@example.com"
}

Sanitized, typed, ready to use. TypeScript knows result.value exists.

FAIL
{
  ok: false,
  errors: [
    {
      rule: "email",
      message: "Invalid email",
      path: ["user", "email"]
    }
  ]
}

Every error collected. Path traces nested objects. Perfect for API responses or UI hints.

Roadmap

Where we've been. Where we're going.

v1.0

Core Chains

String, Number, Boolean, Array, Object, Any, Custom chains with full validators and sanitizers.

v1.1

Async & Conditionals

Async validation with .testAsync() and conditional validation with .when().

v1.2

Combinators

Union types with .or() and intersections with .and().

v1.3

i18n Error Messages

Pluggable message resolver for multi-language support. Built-in en, community locale packs.

v1.4

JSON Schema Export

.toJSONSchema() on any chain for auto-generating OpenAPI/Swagger documentation.

v2.0

Date & File Chains

DateChain with .past(), .future(). FileChain with .maxSize(), .mimeType() for multipart validation.