Skip to main content

Overview

Validation rules are functions that validate form field values and return error messages when validation fails. These rules are used by the form engine to provide real-time field validation.

Import

import { required } from '@shared/validation-engine/rules'

Validator Function Type

All validation functions follow this signature:
type ValidatorFnType = (value: string, fieldName: string) => string | null
Parameters:
  • value: The current field value to validate
  • fieldName: The name/ID of the field being validated (used in error messages)
Returns:
  • string: Error message if validation fails
  • null: If validation passes

Available Validators

required()

Validates that a field has a non-empty value.
function required(value: string, fieldName: string): string | null
value
string
required
The field value to validate
fieldName
string
required
The field identifier, used in the error message
Returns:
  • null if the value (after trimming whitespace) is not empty
  • "{fieldName} is required" if the value is empty or contains only whitespace
Example:
import { required } from '@shared/validation-engine/rules'

const error1 = required('John', 'name')
// Returns: null (valid)

const error2 = required('', 'email')
// Returns: "email is required"

const error3 = required('   ', 'password')
// Returns: "password is required" (whitespace-only is invalid)

Usage in Form Schema

Validators are typically defined in the form schema configuration, not called directly:
import useFormEngine from '@app/hooks/useFormEngine'

const formSchema = [
  {
    id: 'email',
    label: 'Email Address',
    type: 'email',
    placeholder: 'Enter your email',
    fieldValidators: [
      { type: 'required' },  // Maps to the required() function
      { type: 'minLength', constraints: { minLength: 5 } }
    ]
  },
  {
    id: 'username',
    label: 'Username',
    type: 'text',
    placeholder: 'Choose a username',
    fieldValidators: [
      { type: 'required' }
    ]
  }
]

const { formData, onChange, errors } = useFormEngine(formSchema)

How Validation Works

  1. Schema Definition: You define validators in the form schema using validator type objects:
    { type: 'required' }
    
  2. Compilation: The useFormEngine hook compiles these into actual validator functions using attachValidators
  3. Execution: When a field changes, validators run sequentially:
    for (let validator of fieldSchema.fieldValidators) {
      const error = validator(value, fieldId)
      if (error) {
        // Stop at first error
        break
      }
    }
    
  4. Error Display: The first validation error (if any) is returned and can be displayed to the user

Validator Types Configuration

Validators are configured using these type definitions:
type FieldValidatorType =
  | { type: "required" }
  | { type: "minLength", constraints: { minLength: number } }
  | { type: "maxLength", constraints: { minLength: number } }

Required Validator

{
  type: 'required'
}
No additional configuration needed.

MinLength Validator

{
  type: 'minLength',
  constraints: {
    minLength: 8  // Minimum number of characters
  }
}

MaxLength Validator

{
  type: 'maxLength',
  constraints: {
    minLength: 100  // Maximum number of characters
  }
}

Creating Custom Validators

To create a custom validator, follow the ValidatorFnType signature:
function email(value: string, fieldName: string): string | null {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  
  if (!emailRegex.test(value)) {
    return `${fieldName} must be a valid email address`
  }
  
  return null
}

Factory Pattern for Configurable Validators

type ValidatorFactory = (constraints?: any) => ValidatorFnType

function minLength(constraints: { minLength: number }): ValidatorFnType {
  return (value: string, fieldName: string) => {
    if (value.length < constraints.minLength) {
      return `${fieldName} must be at least ${constraints.minLength} characters`
    }
    return null
  }
}

// Usage
const validator = minLength({ minLength: 8 })
const error = validator('short', 'password')
// Returns: "password must be at least 8 characters"

Validation Flow Example

Here’s how validation works in a complete form:
import useFormEngine from '@app/hooks/useFormEngine'

function RegistrationForm() {
  const schema = [
    {
      id: 'email',
      fieldValidators: [{ type: 'required' }]
    },
    {
      id: 'password',
      fieldValidators: [
        { type: 'required' },
        { type: 'minLength', constraints: { minLength: 8 } }
      ]
    }
  ]

  const { formData, onChange, errors } = useFormEngine(schema)

  return (
    <form>
      <input
        type="email"
        value={formData.email || ''}
        onChange={(e) => onChange(e, 'email')}
      />
      {/* Shows: "email is required" if empty */}
      {errors.email && <span>{errors.email}</span>}

      <input
        type="password"
        value={formData.password || ''}
        onChange={(e) => onChange(e, 'password')}
      />
      {/* Shows first error: "password is required" OR 
          "password must be at least 8 characters" */}
      {errors.password && <span>{errors.password}</span>}
    </form>
  )
}

Best Practices

  1. Order Matters: Place validators in order of priority. The first failing validator determines the error message:
    fieldValidators: [
      { type: 'required' },      // Check required first
      { type: 'minLength', ... } // Then check length
    ]
    
  2. Clear Error Messages: Use descriptive field names that make sense in error messages:
    { id: 'email', ... }  // Good: "email is required"
    { id: 'field1', ... } // Bad: "field1 is required"
    
  3. Whitespace Handling: The required validator trims whitespace, preventing users from bypassing validation with spaces
  4. Combine Multiple Validators: Use multiple validators for comprehensive validation:
    fieldValidators: [
      { type: 'required' },
      { type: 'minLength', constraints: { minLength: 8 } },
      { type: 'maxLength', constraints: { minLength: 50 } }
    ]
    

Source

Implemented in: src/shared/validation-engine/rules.ts:1

Build docs developers (and LLMs) love