Form Validation

Implement robust client-side and server-side validation for better user experience and data integrity.

Overview

Proper form validation is crucial for user experience and data integrity. Aki UI provides flexible validation patterns that work with popular libraries like React Hook Form, Formik, and custom validation logic.

Client-sideServer-sideReal-timeAccessible

Validation Approaches

Client-side

Immediate feedback as users type or interact with form fields.

  • • Real-time validation
  • • Better UX
  • • Reduced server load

Server-side

Authoritative validation on the server for security and data integrity.

  • • Security enforcement
  • • Business rule validation
  • • Database constraints

Progressive

Show validation messages after user interaction, not immediately.

  • • Less intrusive
  • • Better accessibility
  • • Contextual feedback

Client-side Validation

Custom Validation Logic

Implement real-time validation with custom validators and progressive disclosure.

import { useState } from 'react'
import { FormControl, Input, Button } from '@akitectio/aki-ui'

function ClientValidationForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    confirmPassword: ''
  })
  const [errors, setErrors] = useState<Record<string, string>>({})
  const [touched, setTouched] = useState<Record<string, boolean>>({})

  const validators = {
    email: (value: string) => {
      if (!value) return 'Email is required'
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
        return 'Please enter a valid email address'
      }
      return ''
    },
    password: (value: string) => {
      if (!value) return 'Password is required'
      if (value.length < 8) return 'Password must be at least 8 characters'
      if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
        return 'Password must contain uppercase, lowercase, and number'
      }
      return ''
    },
    confirmPassword: (value: string) => {
      if (!value) return 'Please confirm your password'
      if (value !== formData.password) return 'Passwords do not match'
      return ''
    }
  }

  const validateField = (name: string, value: string) => {
    const validator = validators[name as keyof typeof validators]
    return validator ? validator(value) : ''
  }

  const handleChange = (name: string, value: string) => {
    setFormData(prev => ({ ...prev, [name]: value }))
    
    if (touched[name]) {
      const error = validateField(name, value)
      setErrors(prev => ({ ...prev, [name]: error }))
    }
  }

  const handleBlur = (name: string) => {
    setTouched(prev => ({ ...prev, [name]: true }))
    const error = validateField(name, formData[name as keyof typeof formData])
    setErrors(prev => ({ ...prev, [name]: error }))
  }

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    
    // Validate all fields
    const newErrors: Record<string, string> = {}
    Object.keys(formData).forEach(key => {
      const error = validateField(key, formData[key as keyof typeof formData])
      if (error) newErrors[key] = error
    })
    
    setErrors(newErrors)
    setTouched(Object.keys(formData).reduce((acc, key) => ({ ...acc, [key]: true }), {}))
    
    if (Object.keys(newErrors).length === 0) {
      // Form is valid, submit data
      console.log('Form submitted:', formData)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <FormControl isInvalid={touched.email && !!errors.email}>
        <FormControl.Label>Email Address</FormControl.Label>
        <Input
          type="email"
          value={formData.email}
          onChange={(e) => handleChange('email', e.target.value)}
          onBlur={() => handleBlur('email')}
        />
        {touched.email && errors.email && (
          <FormControl.ErrorMessage>{errors.email}</FormControl.ErrorMessage>
        )}
      </FormControl>

      <FormControl isInvalid={touched.password && !!errors.password}>
        <FormControl.Label>Password</FormControl.Label>
        <Input
          type="password"
          value={formData.password}
          onChange={(e) => handleChange('password', e.target.value)}
          onBlur={() => handleBlur('password')}
        />
        {touched.password && errors.password && (
          <FormControl.ErrorMessage>{errors.password}</FormControl.ErrorMessage>
        )}
      </FormControl>

      <FormControl isInvalid={touched.confirmPassword && !!errors.confirmPassword}>
        <FormControl.Label>Confirm Password</FormControl.Label>
        <Input
          type="password"
          value={formData.confirmPassword}
          onChange={(e) => handleChange('confirmPassword', e.target.value)}
          onBlur={() => handleBlur('confirmPassword')}
        />
        {touched.confirmPassword && errors.confirmPassword && (
          <FormControl.ErrorMessage>{errors.confirmPassword}</FormControl.ErrorMessage>
        )}
      </FormControl>

      <Button 
        type="submit" 
        disabled={Object.keys(errors).some(key => errors[key])}
      >
        Create Account
      </Button>
    </form>
  )
}

Server-side Validation

Handling Server Errors

Handle validation errors from API endpoints and display them appropriately.

import { useState } from 'react'
import { FormControl, Input, Button, Alert } from '@akitectio/aki-ui'

function ServerValidationForm() {
  const [formData, setFormData] = useState({ email: '', password: '' })
  const [errors, setErrors] = useState<Record<string, string>>({})
  const [serverError, setServerError] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setIsSubmitting(true)
    setServerError('')
    setErrors({})

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      })

      const result = await response.json()

      if (!response.ok) {
        if (result.fieldErrors) {
          // Field-specific errors from server
          setErrors(result.fieldErrors)
        } else {
          // General server error
          setServerError(result.message || 'An error occurred')
        }
      } else {
        // Success
        console.log('Login successful:', result)
      }
    } catch (error) {
      setServerError('Network error. Please try again.')
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      {serverError && (
        <Alert variant="destructive">
          <ExclamationCircleIcon className="w-4 h-4" />
          {serverError}
        </Alert>
      )}

      <FormControl isInvalid={!!errors.email}>
        <FormControl.Label>Email Address</FormControl.Label>
        <Input
          type="email"
          value={formData.email}
          onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
          disabled={isSubmitting}
        />
        {errors.email && (
          <FormControl.ErrorMessage>{errors.email}</FormControl.ErrorMessage>
        )}
      </FormControl>

      <FormControl isInvalid={!!errors.password}>
        <FormControl.Label>Password</FormControl.Label>
        <Input
          type="password"
          value={formData.password}
          onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
          disabled={isSubmitting}
        />
        {errors.password && (
          <FormControl.ErrorMessage>{errors.password}</FormControl.ErrorMessage>
        )}
      </FormControl>

      <Button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Signing In...' : 'Sign In'}
      </Button>
    </form>
  )
}

Schema Validation with Zod

Type-safe Validation

Use Zod with React Hook Form for powerful, type-safe validation schemas.

import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { FormControl, Input, Button } from '@akitectio/aki-ui'

const registrationSchema = z.object({
  email: z.string().email('Please enter a valid email address'),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/(?=.*[a-z])/, 'Password must contain a lowercase letter')
    .regex(/(?=.*[A-Z])/, 'Password must contain an uppercase letter')
    .regex(/(?=.*\d)/, 'Password must contain a number'),
  confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"]
})

type RegistrationData = z.infer<typeof registrationSchema>

function ZodValidationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<RegistrationData>({
    resolver: zodResolver(registrationSchema)
  })

  const onSubmit = async (data: RegistrationData) => {
    try {
      const response = await fetch('/api/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      })
      
      if (response.ok) {
        console.log('Registration successful')
      }
    } catch (error) {
      console.error('Registration failed:', error)
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <FormControl isInvalid={!!errors.email}>
        <FormControl.Label>Email Address</FormControl.Label>
        <Input
          type="email"
          {...register('email')}
        />
        {errors.email && (
          <FormControl.ErrorMessage>{errors.email.message}</FormControl.ErrorMessage>
        )}
      </FormControl>

      <FormControl isInvalid={!!errors.password}>
        <FormControl.Label>Password</FormControl.Label>
        <Input
          type="password"
          {...register('password')}
        />
        {errors.password && (
          <FormControl.ErrorMessage>{errors.password.message}</FormControl.ErrorMessage>
        )}
      </FormControl>

      <FormControl isInvalid={!!errors.confirmPassword}>
        <FormControl.Label>Confirm Password</FormControl.Label>
        <Input
          type="password"
          {...register('confirmPassword')}
        />
        {errors.confirmPassword && (
          <FormControl.ErrorMessage>{errors.confirmPassword.message}</FormControl.ErrorMessage>
        )}
      </FormControl>

      <Button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Creating Account...' : 'Create Account'}
      </Button>
    </form>
  )
}

Validation Best Practices

Best Practices

  • • Validate on blur for better UX
  • • Show success states for completed fields
  • • Use clear, actionable error messages
  • • Implement both client and server validation
  • • Provide inline help for complex requirements
  • • Consider accessibility and screen readers
  • • Test validation with edge cases

Common Pitfalls

  • • Validating on every keystroke
  • • Showing errors before user interaction
  • • Using technical error messages
  • • Relying only on client-side validation
  • • Not handling network errors gracefully
  • • Inconsistent validation timing
  • • Poor error message placement

Popular Libraries

React Hook Form

Performant forms with easy validation and minimal re-renders.

Recommended

Formik

Popular form library with built-in validation and error handling.

Supported

Zod + RHF

Type-safe schema validation with React Hook Form integration.

Type-safe