Skip to main content
The Auction Platform includes a flexible validation engine for form validation. It uses a registry pattern to map validation rule types to validator functions.

Architecture

The validation engine consists of three main components:
  1. Validator Registry - Maps rule types to validator factory functions
  2. Validation Rules - Type definitions for validation constraints
  3. Attach Validators - Utility to attach validators to form fields

Type Definitions

The validation system uses TypeScript for type safety:
src/shared/validation-engine/types/rules.type.ts
export type ValidatorFnType = (value: string, fieldName: string) => string | null

export type FieldValidatorType =
  | { type: "required" }
  | { type: "minLength", constraints: { minLength: number } }
  | { type: "maxLength", constraints: { minLength: number } }

export type BaseField = {
  id: string
  fieldValidators: FieldValidatorType[]
}

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

export type LengthConstraints = {
  minLength: number
}

Key Types

ValidatorFnType
function
A validator function that takes a value and field name, returns an error message or null
FieldValidatorType
union
Union type defining all available validation rules (required, minLength, maxLength)
BaseField
object
Base structure for a form field with validation rules
ValidatorFactory
function
Factory function that creates a validator with optional constraints

Validator Registry

The validator registry maps rule types to validator factory functions:
src/shared/validation-engine/validatiorRegistery.ts
import type { LengthConstraints, ValidatorFactory } from "./types/rules.type"

export const validatorRegistery: Record<string, ValidatorFactory> = {
  required: () => {
    return (value: string) => {
      if (!value) return "Field is required"
      return null
    }
  },
  minLength: (constraints: LengthConstraints) => {
    return (value: string) => {
      if (value.trim().length < constraints.minLength) {
        return `Must be at least ${constraints.minLength} characters`
      }
      return null
    }
  },
  maxLength: (constraints: LengthConstraints) => {
    return (value: string) => {
      if (value.trim().length > constraints.minLength) {
        return `Must be less than ${constraints.minLength} characters`
      }
      return null
    }
  }
}

Built-in Validators

required

Ensures the field has a valueReturns: “Field is required” if empty

minLength

Ensures minimum character countConstraints: { minLength: number }

maxLength

Ensures maximum character countConstraints: { minLength: number }

Attaching Validators

The attachValidators function converts validation rules into validator functions:
src/shared/validation-engine/attachValidators.ts
import type { BaseField, ValidatorFnType } from "./types/rules.type"
import { validatorRegistery } from "./validatiorRegistery"

export function attachValidators<T extends BaseField>(
  field: T
): Omit<T, "fieldValidators"> & { fieldValidators: ValidatorFnType[] } {
  return {
    ...field,
    fieldValidators: field.fieldValidators.map((rule) => {
      const factory = validatorRegistery[rule.type]

      if ("constraints" in rule) {
        return factory(rule.constraints)
      }

      return factory()
    })
  }
}

How It Works

  1. Takes a field with declarative validation rules
  2. Looks up each rule type in the validator registry
  3. Creates validator functions using the factory with constraints if provided
  4. Returns the field with executable validator functions

Usage Examples

Basic Field Validation

import { attachValidators } from '@shared/validation-engine/attachValidators';

// Define a field with validation rules
const emailField = {
  id: 'email',
  fieldValidators: [
    { type: 'required' as const },
    { type: 'minLength' as const, constraints: { minLength: 5 } }
  ]
};

// Attach validators
const validatedField = attachValidators(emailField);

// Run validators
const errors = validatedField.fieldValidators
  .map(validator => validator('test', 'Email'))
  .filter(error => error !== null);

if (errors.length > 0) {
  console.log('Validation errors:', errors);
}

Form with Multiple Fields

const formFields = [
  {
    id: 'username',
    label: 'Username',
    fieldValidators: [
      { type: 'required' as const },
      { type: 'minLength' as const, constraints: { minLength: 3 } },
      { type: 'maxLength' as const, constraints: { minLength: 20 } }
    ]
  },
  {
    id: 'password',
    label: 'Password',
    fieldValidators: [
      { type: 'required' as const },
      { type: 'minLength' as const, constraints: { minLength: 8 } }
    ]
  }
];

// Attach validators to all fields
const validatedFields = formFields.map(attachValidators);

// Validation function
function validateField(field: typeof validatedFields[0], value: string) {
  const errors = field.fieldValidators
    .map(validator => validator(value, field.label))
    .filter(Boolean);
  
  return errors[0] || null; // Return first error
}

React Hook Integration

import { useState } from 'react';
import { attachValidators } from '@shared/validation-engine/attachValidators';

function useFieldValidation(fieldDefinition) {
  const [value, setValue] = useState('');
  const [error, setError] = useState<string | null>(null);
  
  const validatedField = attachValidators(fieldDefinition);
  
  const validate = (newValue: string) => {
    setValue(newValue);
    
    // Run all validators
    const errors = validatedField.fieldValidators
      .map(validator => validator(newValue, fieldDefinition.id))
      .filter(Boolean);
    
    setError(errors[0] || null);
    return errors.length === 0;
  };
  
  return { value, error, validate };
}

// Usage in component
function SignupForm() {
  const username = useFieldValidation({
    id: 'username',
    fieldValidators: [
      { type: 'required' as const },
      { type: 'minLength' as const, constraints: { minLength: 3 } }
    ]
  });
  
  return (
    <div>
      <input 
        value={username.value}
        onChange={e => username.validate(e.target.value)}
      />
      {username.error && <span>{username.error}</span>}
    </div>
  );
}

Creating Custom Validators

Extend the validation engine with custom validators:

Step 1: Add Type Definition

types/rules.type.ts
export type FieldValidatorType =
  | { type: "required" }
  | { type: "minLength", constraints: { minLength: number } }
  | { type: "maxLength", constraints: { minLength: number } }
  | { type: "email" }  // New validator
  | { type: "pattern", constraints: { pattern: RegExp, message: string } }  // New

Step 2: Register Validator

validatiorRegistery.ts
import type { ValidatorFactory } from "./types/rules.type"

export const validatorRegistery: Record<string, ValidatorFactory> = {
  // ... existing validators
  
  email: () => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return (value: string) => {
      if (value && !emailRegex.test(value)) {
        return "Please enter a valid email address"
      }
      return null
    }
  },
  
  pattern: (constraints: { pattern: RegExp, message: string }) => {
    return (value: string) => {
      if (value && !constraints.pattern.test(value)) {
        return constraints.message
      }
      return null
    }
  },
  
  url: () => {
    const urlRegex = /^https?:\/\/.+/;
    return (value: string) => {
      if (value && !urlRegex.test(value)) {
        return "Please enter a valid URL"
      }
      return null
    }
  },
  
  numeric: () => {
    return (value: string) => {
      if (value && isNaN(Number(value))) {
        return "Please enter a number"
      }
      return null
    }
  }
}

Step 3: Use Custom Validator

const emailField = {
  id: 'email',
  fieldValidators: [
    { type: 'required' as const },
    { type: 'email' as const }  // Use custom email validator
  ]
};

const websiteField = {
  id: 'website',
  fieldValidators: [
    { type: 'url' as const }
  ]
};

const bidAmountField = {
  id: 'bidAmount',
  fieldValidators: [
    { type: 'required' as const },
    { type: 'numeric' as const },
    { 
      type: 'pattern' as const, 
      constraints: { 
        pattern: /^[1-9]\d*$/, 
        message: 'Bid must be a positive number' 
      }
    }
  ]
};

Advanced Patterns

Async Validators

export const validatorRegistery: Record<string, ValidatorFactory> = {
  uniqueUsername: () => {
    return async (value: string) => {
      const response = await fetch(`/api/check-username?username=${value}`);
      const { exists } = await response.json();
      
      if (exists) {
        return "Username already taken"
      }
      return null
    }
  }
}

Dependent Field Validation

export const validatorRegistery: Record<string, ValidatorFactory> = {
  matchField: (constraints: { fieldToMatch: string, label: string }) => {
    return (value: string, fieldName: string, formValues: Record<string, string>) => {
      if (value !== formValues[constraints.fieldToMatch]) {
        return `${fieldName} must match ${constraints.label}`
      }
      return null
    }
  }
}

// Usage: password confirmation
const confirmPasswordField = {
  id: 'confirmPassword',
  fieldValidators: [
    { type: 'required' as const },
    { 
      type: 'matchField' as const, 
      constraints: { fieldToMatch: 'password', label: 'Password' }
    }
  ]
};

Best Practices

Always return null for valid values, not empty strings or undefined:
// Good
if (isValid) return null;

// Avoid
if (isValid) return '';
Error messages should be user-friendly and actionable:
// Good
return "Must be at least 8 characters";

// Avoid
return "Invalid";
Always trim whitespace when checking string length:
if (value.trim().length < minLength) {
  return `Must be at least ${minLength} characters`;
}
Leverage TypeScript’s type system to ensure validators match field types.

Reference

File Locations

src/shared/validation-engine/
├── attachValidators.ts      # Attaches validators to fields
├── validatiorRegistery.ts   # Validator registry
├── rules.ts                 # Legacy rule functions
└── types/
    └── rules.type.ts        # Type definitions
The validation engine is framework-agnostic and can be used with any form library or custom form solution.

Build docs developers (and LLMs) love