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.
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.
RecommendedFormik
Popular form library with built-in validation and error handling.
SupportedZod + RHF
Type-safe schema validation with React Hook Form integration.
Type-safe