Server-side Validation
typescript
1import { Form, useFormContext } from '@flint-ui/core'23interface ServerError {4 field: string5 code: string6 message: string7}89function mapServerErrors(errors: ServerError[]) {10 return Object.fromEntries(11 errors.map((e) => [e.field, e.message])12 )13}1415function ServerValidatedForm() {16 const handleSubmit = async (data: FormData) => {17 const res = await fetch('/api/register', {18 method: 'POST',19 body: JSON.stringify(data),20 })2122 if (res.status === 422) {23 const { errors } = await res.json()24 // Return field errors to set them on the form25 return { fieldErrors: mapServerErrors(errors) }26 }2728 if (!res.ok) throw new Error('Request failed')29 }3031 return (32 <Form onSubmit={handleSubmit} validateOn="onSubmit">33 <Input name="username" label="Username" />34 <Input name="email" label="Email" />35 <Button type="submit">Register</Button>36 </Form>37 )38}
Mapping API 422 errors back to individual form fields
Overview
Server-side validation catches errors that client-side checks cannot: duplicate emails, expired tokens, database constraints, and business logic rules. Flint UI's Input component integrates cleanly with server responses.
Handling Server Errors
Map server error responses to individual field errors. This pattern works with any API that returns field-level error messages.
typescript
1import { useState } from 'react'2import { Input, Button } from '@flint-ui/core'34type FieldErrors = Record<string, string>56function RegistrationForm() {7 const [errors, setErrors] = useState<FieldErrors>({})8 const [loading, setLoading] = useState(false)910 const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {11 e.preventDefault()12 setErrors({})13 setLoading(true)1415 const formData = new FormData(e.currentTarget)1617 try {18 const res = await fetch('/api/register', {19 method: 'POST',20 body: formData,21 })2223 if (!res.ok) {24 const data = await res.json()25 // Server returns: { errors: { email: "Already taken", password: "Too weak" } }26 setErrors(data.errors ?? {})27 }28 } finally {29 setLoading(false)30 }31 }3233 return (34 <form onSubmit={handleSubmit} noValidate>35 <Input36 name="email"37 label="Email"38 type="email"39 error={errors.email}40 aria-invalid={!!errors.email}41 />42 <Input43 name="password"44 label="Password"45 type="password"46 error={errors.password}47 aria-invalid={!!errors.password}48 />49 <Button type="submit" loading={loading} tone="primary">50 Register51 </Button>52 </form>53 )54}
Inline Validation with Debounce
For fields like username or email, check availability as the user types. Debounce the API call to avoid excessive requests.
typescript
1import { useCallback, useEffect, useState } from 'react'23function useFieldValidation(value: string, endpoint: string, delay = 500) {4 const [error, setError] = useState<string | null>(null)5 const [checking, setChecking] = useState(false)67 useEffect(() => {8 if (!value) { setError(null); return }910 const timer = setTimeout(async () => {11 setChecking(true)12 try {13 const res = await fetch(`\${endpoint}?value=\${encodeURIComponent(value)}`)14 const data = await res.json()15 setError(data.available ? null : data.message)16 } finally {17 setChecking(false)18 }19 }, delay)2021 return () => clearTimeout(timer)22 }, [value, endpoint, delay])2324 return { error, checking }25}
Error Display Patterns
- Show field-level errors inline, directly below the relevant input
- Use
aria-invalidandaria-describedbyfor screen reader support - Clear field errors when the user starts editing that field
- Show a global error banner for non-field errors (network failures, rate limits)