UNPKG

snap-validate

Version:

Zero-dependency, ~4KB validation library for common patterns. CJS + ESM, Standard Schema support, no TypeScript compiler required.

606 lines (451 loc) โ€ข 21.5 kB
# Snap Validate โšก [![npm version](https://img.shields.io/npm/v/snap-validate.svg?style=flat-square)](https://www.npmjs.com/package/snap-validate) [![Build Status](https://github.com/aniru-dh21/snap-validate/workflows/CI%2FCD%20Pipeline/badge.svg)](https://github.com/aniru-dh21/snap-validate/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![install size](https://img.shields.io/badge/dynamic/json?url=https://packagephobia.com/v2/api.json?p=snap-validate&query=$.install.pretty&label=install%20size&style=flat-square)](https://packagephobia.now.sh/result?p=snap-validate) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/snap-validate?style=flat-square)](https://bundlephobia.com/package/snap-validate@latest) [![npm downloads](https://img.shields.io/npm/dm/snap-validate.svg?style=flat-square)](https://npm-stat.com/charts.html?package=snap-validate) ![Codecov](https://img.shields.io/codecov/c/github/aniru-dh21/snap-validate) A lightning-fast, lightweight validation library for common patterns without heavy dependencies โ€” **zero dependencies, ~4 KB min+gzip for the entire library, no tree-shaking required**. Validation for people who don't want a TypeScript compiler, a schema DSL, or a 15 KB dependency: just call a function. Works in Node (CJS **and** ESM) and in browsers via any bundler, with built-in guards against ReDoS (Regular Expression Denial of Service) patterns and first-class [Standard Schema](https://standardschema.dev) support for use with tRPC, TanStack Form, Hono, and friends. ## Features - โšก **Lightning Fast**: Optimized for speed and performance - ๐Ÿš€ **Lightweight**: No external dependencies, minimal footprint - ๐Ÿ”ง **Flexible**: Chainable validation rules and custom validators - ๐Ÿ“ง **Common Patterns**: Email, phone, credit card, URL, password validation - ๐ŸŒ **International**: Support for different formats (US/International phone, postal codes) - ๐Ÿ”„ **Async Support**: Full async validation support for database checks and API calls - ๐ŸŽฏ **Conditional**: Advanced conditional validation with `when()` and `optional()` - ๐Ÿ› ๏ธ **Custom Validators**: Add your own sync and async validation logic - ๐Ÿ”’ **Security First**: Built-in protection against ReDoS attacks and unsafe regex patterns - ๐Ÿ›ก๏ธ **Input Guards**: Input-length limits and static detection of dangerous regex shapes - ๐Ÿงช **Well Tested**: Comprehensive test suite with high coverage - ๐Ÿ“ฆ **Easy Integration**: Works in Node.js and browsers - ๐Ÿ”— **Chainable API**: Intuitive fluent interface - ๐Ÿ“ฆ **CJS + ESM**: `require()` or `import` โ€” one implementation, both module systems - ๐Ÿค **Standard Schema**: Implements [Standard Schema v1](https://standardschema.dev) for drop-in use with tRPC, TanStack Form, Hono, and other compatible tools - ๐Ÿ“˜ **TypeScript Support**: Complete TypeScript definitions with full IntelliSense support ## Installation ```bash npm install snap-validate ``` Runtime: Node.js **>= 18** (the published library has zero dependencies and also runs in browsers via a bundler โ€” see [Browser Usage](#browser-usage)). Both module systems are supported out of the box: ```javascript // CommonJS const { validators, validate } = require('snap-validate'); // ESM import { validators, validate } from 'snap-validate'; ``` ### TypeScript For TypeScript projects, types are included automatically: ```bash npm install snap-validate # Types are included - no need for @types/snap-validate ``` ## Quick Start ```javascript const { validators, validate } = require('snap-validate'); // Single field validation const emailResult = validators.email('user@example.com').validate(); console.log(emailResult.isValid); // true // Schema validation const schema = { email: validators.email, phone: (value) => validators.phone(value, 'us'), password: validators.password }; const data = { email: 'user@example.com', phone: '(555) 123-4567', password: 'SecurePass123' }; const result = validate(schema, data); console.log(result.isValid); // true ``` ## Standard Schema Support snap-validate implements [Standard Schema v1](https://standardschema.dev) โ€” the common interface consumed by tRPC, TanStack Form, Hono, and a growing list of tools. Wrap any validator factory or schema object with `toStandardSchema()` and pass it to anything that accepts a Standard Schema: ```javascript import { validators, BaseValidator, toStandardSchema } from 'snap-validate'; // Single-value schema (reusable, concurrency-safe) const emailSchema = toStandardSchema((value) => validators.email(value)); // Object schema โ€” issues carry field paths, transforms flow into the output const signupSchema = toStandardSchema({ email: validators.email, age: (value) => new BaseValidator(value).between(18, 99) }); // Any Standard Schema consumer can now use these: const result = signupSchema['~standard'].validate({ email: 'A@B.com', age: 30 }); // -> { value: { email: 'a@b.com', age: 30 } } on success // -> { issues: [{ message, path: ['email'] }, ...] } on failure ``` Example with a Standard Schema-aware framework (TanStack Form): ```javascript import { useForm } from '@tanstack/react-form'; const form = useForm({ validators: { onChange: signupSchema } // snap-validate, no adapter needed }); ``` Sync validators return results synchronously; validators with async rules (`customAsync`, `patternAsync`, `arrayOfAsync`, `objectAsync`) return a Promise, per the spec. `BaseValidator` instances also expose `~standard` directly for tools that duck-type on the property โ€” use `toStandardSchema()` when the same schema validates many values concurrently. ## TypeScript Support Snap Validate includes comprehensive TypeScript definitions for enhanced developer experience: ```typescript import { BaseValidator, validators, validate, ValidationResult } from 'snap-validate'; // Full type safety and auto-completion const validator = new BaseValidator('test-value') .required('This field is required') .min(5, 'Must be at least 5 characters') .pattern(/^[a-zA-Z]+$/, 'Only letters allowed'); // Type-safe result handling const result: ValidationResult = validator.validate(); // Schema validation with types interface UserData { email: string; phone: string; password: string; } const userData: UserData = { email: 'john@example.com', phone: '1234567890', password: 'StrongPass123' }; const schema = { email: validators.email, phone: (value: string) => validators.phone(value, 'us'), password: validators.password }; const result = validate(schema, userData); ``` Features: - Complete type definitions for all classes and functions - IntelliSense support in VS Code, WebStorm, and other editors - Compile-time validation prevents common usage errors - Generic support for flexible validation workflows - Rich JSDoc comments for comprehensive documentation ## Security Features ### ReDoS Protection Snap Validate includes built-in protection against Regular Expression Denial of Service (ReDoS) attacks: - **Regex Safety Detection**: Best-effort static heuristic (`isRegexSafe`) that flags a few common catastrophic-backtracking shapes before a pattern is used - **Input Length Limits**: Protects against extremely long input strings (10,000 character limit) - **Safe Defaults**: All predefined validators use safe, optimized regex patterns > **A note on timeouts:** earlier versions advertised a configurable regex "timeout." JavaScript runs regexes synchronously on a single thread, so a timer cannot interrupt a regex that is mid-backtrack โ€” the event loop is blocked until it finishes. The timeout therefore never provided real protection and has been deprecated (see the CHANGELOG). The effective guards are the input-length cap and the `isRegexSafe` static check. For guaranteed linear-time matching, use a non-backtracking engine such as the native `re2` module. ```javascript const { BaseValidator, isRegexSafe } = require('snap-validate'); // Check a custom pattern before using it if (isRegexSafe(/^[a-zA-Z0-9]+$/)) { const validator = new BaseValidator(value) .pattern(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters allowed'); } // Async pattern validation (applies the same length + safety guards) const asyncValidator = new BaseValidator(value) .patternAsync(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters allowed'); const result = await asyncValidator.validateAsync(); ``` ## Available Validators ### Email Validation ```javascript validators.email('user@example.com').validate(); ``` ### Phone Number Validation ```javascript // US format (default) validators.phone('(555) 123-4567').validate(); // International format validators.phone('+1234567890', 'international').validate(); // Simple numeric validators.phone('1234567890', 'simple').validate(); ``` ### Credit Card Validation ```javascript // Uses Luhn algorithm validators.creditCard('4532015112830366').validate(); ``` ### URL Validation ```javascript validators.url('https://example.com').validate(); ``` ### Password Validation ```javascript // Default: min 8 chars, requires upper, lower, numbers validators.password('SecurePass123').validate(); // Custom options validators.password('MyPass123!', { minLength: 10, requireUppercase: true, requireLowercase: true, requireNumbers: true, requireSpecialChars: true }).validate(); ``` ### Alphanumeric Validation ```javascript validators.alphanumeric('ABC123').validate(); ``` ### Numeric Validation ```javascript validators.numeric('12345').validate(); ``` ### Zip Code Validation ```javascript // US zip code validators.zipCode('12345').validate(); validators.zipCode('12345-6789').validate(); // Canadian postal code validators.zipCode('K1A 0A6', 'ca').validate(); // UK postal code validators.zipCode('SW1A 1AA', 'uk').validate(); ``` ## Advanced Validation Features ### Conditional Validation ```javascript const { BaseValidator } = require('snap-validate'); // Validate only when condition is met const validator = new BaseValidator(value) .when(user.isAdmin, validators.required('Admin field required')) .min(5, 'Must be at least 5 characters'); // Optional validation - skip if empty/null/undefined const optionalValidator = new BaseValidator(value) .optional() .pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Must be a valid email if provided'); // Function-based conditions const conditionalValidator = new BaseValidator(value) .when(() => user.role === 'admin', validators.required()) .max(100); ``` ### Custom Validators ```javascript const { BaseValidator, validators } = require('snap-validate'); // Synchronous custom validation const customValidator = new BaseValidator(value) .custom((val) => val !== 'forbidden', 'Value cannot be forbidden') .custom((val) => { if (val.includes('admin') && !user.isAdmin) { return 'Only admins can use this value'; } return true; }); // Asynchronous custom validation (validators.email returns a BaseValidator you can chain) const asyncValidator = validators.email(email) .customAsync(async (email) => { const exists = await checkEmailExists(email); return !exists || 'Email already exists'; }, 'Email validation failed'); // Use async validation const result = await asyncValidator.validateAsync(); ``` ### Async Validation ```javascript const { BaseValidator, validators, validateAsync } = require('snap-validate'); // Async validation for single field const validator = new BaseValidator(username) .required() .min(3) .customAsync(async (username) => { const available = await checkUsernameAvailable(username); return available || 'Username is already taken'; }); const result = await validator.validateAsync(); // Async schema validation const asyncSchema = { username: (value) => new BaseValidator(value) .required() .customAsync(async (val) => { const available = await checkUsernameAvailable(val); return available || 'Username taken'; }), email: (value) => validators.email(value) .customAsync(async (val) => { const exists = await checkEmailExists(val); return !exists || 'Email already registered'; }) }; const asyncResult = await validateAsync(asyncSchema, userData); ``` ## Security and Pattern Validation ### Safe Pattern Validation ```javascript const { BaseValidator } = require('snap-validate'); // Synchronous pattern validation with built-in safety checks const validator = new BaseValidator(value) .pattern(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters allowed'); // Asynchronous pattern validation (same length + static-safety guards) const asyncValidator = new BaseValidator(value) .patternAsync(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters allowed'); const result = await asyncValidator.validateAsync(); ``` ### How the safety guards work ```javascript const validator = new BaseValidator(value) .pattern(/your-pattern/, 'Error message'); // For every pattern, the library automatically: // - Runs a best-effort static check (isRegexSafe) that rejects a few common // catastrophic-backtracking shapes (throws on a flagged pattern) // - Limits input length to 10,000 characters to bound matching work // - Provides clear error messages for security violations // // Note: there is no runtime timeout - a timer cannot interrupt a synchronous // regex on a single thread. See the Security Features section above. ``` ## Custom Validation ### Using BaseValidator ```javascript const { BaseValidator } = require('snap-validate'); const customValidator = new BaseValidator('test-value') .required('This field is required') .min(5, 'Must be at least 5 characters') .max(20, 'Must be no more than 20 characters') .pattern(/^[a-zA-Z]+$/, 'Only letters allowed'); const result = customValidator.validate(); ``` ### Schema Validation with Custom Rules ```javascript const schema = { username: (value) => new BaseValidator(value) .required() .min(3) .max(20) .pattern(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'), email: validators.email, age: (value) => new BaseValidator(value) .required() .pattern(/^\d+$/, 'Age must be a number') .custom((val) => parseInt(val) >= 18, 'Must be 18 or older') }; const userData = { username: 'john_doe', email: 'john@example.com', age: '25' }; const result = validate(schema, userData); ``` ## Error Handling ```javascript const result = validators.email('invalid-email').validate(); if (!result.isValid) { console.log('Validation errors:', result.errors); // Output: ['Invalid email format'] } // For schema validation const schemaResult = validate(schema, data); if (!schemaResult.isValid) { const errors = schemaResult.getErrors(); console.log('Field errors:', errors); // Output: { email: ['Invalid email format'], password: ['Password too weak'] } } // Async error handling try { const asyncResult = await validator.validateAsync(); if (!asyncResult.isValid) { console.log('Async validation errors:', asyncResult.errors); } } catch (error) { console.log('Validation exception:', error.message); } // Security-related errors const unsafeResult = validator.pattern(/potentially-dangerous-pattern/, 'Error').validate(); if (!unsafeResult.isValid) { console.log('Security errors:', unsafeResult.errors); // Output: ['Potentially unsafe regex pattern detected'] } ``` ## Browser Usage Snap Validate ships as CommonJS modules (`require`/`module.exports`). In the browser, use it through a bundler that understands CommonJS โ€” such as [Vite](https://vitejs.dev/), [webpack](https://webpack.js.org/), [esbuild](https://esbuild.github.io/), or [Rollup](https://rollupjs.org/): ```javascript import { validators } from 'snap-validate'; const result = validators.email('user@example.com').validate(); console.log(result.isValid); ``` > A raw `<script src=".../src/index.js">` tag will not work directly, because the source uses `require()` to load its internal modules. Bundle it (or wrap it in your own UMD/ESM build) for direct browser use. ## API Reference ### ValidationResult - `isValid: boolean` - Whether validation passed - `errors: string[]` - Array of error messages ### BaseValidator Methods - `required(message?)` - Field is required - `optional()` - Skip validation if empty/null/undefined - `setFieldName(name)` - Set the field name used to prefix error messages - `transform(fn, errorMessage?)` - Transform/sanitize the value before later rules run - `equals(compareValue, message?)` - Require strict equality with a value - `oneOf(allowedValues, message?)` - Require the value to be one of a set - `between(min, max, message?)` - Require a number within an inclusive range - `min(length, message?)` - Minimum length (string/array) or value (number) - `max(length, message?)` - Maximum length (string/array) or value (number) - `array(message?)` - Require the value to be an array - `arrayOf(validator, message?)` - Validate each item of an array - `arrayOfAsync(validator, message?)` - Validate each item of an array (async) - `object(schema, message?)` - Validate a nested object against a schema - `objectAsync(schema, message?)` - Validate a nested object against a schema (async) - `pattern(regex, message?)` - Pattern matching validation with safety checks - `patternAsync(regex, message?)` - Async pattern validation (length + static-safety guards) - `when(condition, validator)` - Conditional validation - `custom(fn, message?)` - Custom synchronous validation - `customAsync(fn, message?)` - Custom asynchronous validation - `validate()` - Execute synchronous validation - `validateAsync()` - Execute asynchronous validation - `setRegexTimeout(timeoutMs)` - **Deprecated (no-op).** Kept for compatibility; a timer cannot interrupt a synchronous regex, so the value is ignored ### Available Validators - `validators.email(value)` - `validators.phone(value, format?)` - `validators.creditCard(value)` - `validators.url(value)` - `validators.password(value, options?)` - `validators.alphanumeric(value)` - `validators.numeric(value)` - `validators.zipCode(value, country?)` ### TypeScript Types - `ValidationResult` - Class for validation results (`isValid`, `errors`) - `SchemaValidationResult` - Result of `validate` / `validateAsync` (`isValid`, `errors`, `getErrors()`) - `ValidationFunction` - Type for validator functions used in schemas - `Schema` - Type for validation schema objects - `PasswordOptions` - Interface for password validation configuration - `PhoneFormat` / `CountryCode` - String-literal unions for `phone` / `zipCode` options - `CustomValidatorFunction` / `AsyncValidatorFunction` / `TransformFunction` - Callback types - `BaseValidator` - The chainable validator class ### Validation Functions - `validate(schema, data)` - Synchronous schema validation - `validateAsync(schema, data)` - Asynchronous schema validation - `toStandardSchema(factoryOrSchema)` - Wrap a validator factory or schema object as a [Standard Schema v1](https://standardschema.dev) for use with tRPC, TanStack Form, Hono, etc. ### Security Functions - `isRegexSafe(regex)` - Best-effort static check for a few dangerous regex shapes (returns `boolean`) - `safeRegexTest(regex, str)` - Async regex test with input-length + safety guards (returns `Promise<boolean>`). A legacy `timeoutMs` third argument is accepted but ignored (deprecated) - `safeRegexTestSync(regex, str, maxLength?)` - Synchronous regex test with input-length protection (returns `boolean`) ## Security Best Practices 1. **Use Built-in Validators**: The predefined validators are optimized for security and performance 2. **Validate Input Length**: Large inputs are automatically limited to prevent ReDoS attacks 3. **Test Custom Patterns**: Use `isRegexSafe()` to check custom regex patterns before deployment 4. **Handle Async Errors**: Always use try-catch blocks with async validation 5. **Consider a hardened engine**: For untrusted patterns needing guaranteed linear-time matching, pair the library with a non-backtracking engine such as the native `re2` module ## Contributing We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## Development > Development tooling (ESLint 10) requires Node.js **>= 20.19**. This is a contributor requirement only โ€” the published library still supports Node >= 18 for consumers. ```bash # Install dependencies npm install # Run tests npm test # Run tests in watch mode npm run test:watch # Run tests with coverage npm run test:coverage # Lint code npm run lint # Format code npm run format # Security audit npm audit # Type checking (for TypeScript users) npm run type-check # Validate TypeScript definitions npm run validate-types ``` ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Changelog See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes. --- Made with โšก by [Ramachandra Anirudh Vemulapalli](https://github.com/aniru-dh21)