Validation — Rules, Timing and Messages
IntermediateDefine the rules once as a schema, validate on blur rather than on every keystroke, and never treat client validation as security.
Overview
Validation has three separable questions and people usually only think about the first. What are the rules — which should live in one schema rather than scattered through handlers. When do you tell the user — because validating on the first keystroke means shouting "invalid email" at someone who has typed the letter a. And where does the truth live — on the server, always, because client validation is a convenience for honest users and no obstacle at all to anyone else.
One Schema
A schema is a single definition producing both the checks and, in TypeScript, the type.
import { z } from 'zod'
const SignupSchema = z.object({
name: z.string().min(2, 'Please enter your name'),
email: z.string().email('That does not look like an email'),
password: z.string()
.min(8, 'At least 8 characters')
.regex(/[0-9]/, 'Include a number'),
confirm: z.string(),
age: z.coerce.number().int().min(13, 'You must be 13 or older'),
}).refine(v => v.password === v.confirm, {
message: 'Passwords do not match',
path: ['confirm'], // attaches the error to the right field
})
type SignupValues = z.infer<typeof SignupSchema> // the type, for free
// Validating gives you either the parsed values or field errors
const result = SignupSchema.safeParse(values)
if (!result.success) setErrors(result.error.flatten().fieldErrors)
// The same schema can run on a Node backend, which is the strongest
// version of "define the rules once".When to Show an Error
The timing rule that makes a form feel helpful rather than hostile.
// Hostile: validate on every keystroke from the first character
// The user sees "Invalid email" while typing "a", "ak", "aks"…
// The convention that works:
// - validate a field on BLUR (they have finished with it)
// - after it has errored once, re-validate on CHANGE so the error
// clears as soon as they fix it
// - validate everything on SUBMIT
// - move focus to the first field with an error
const [touched, setTouched] = useState({})
const showError = (field) => touched[field] && errors[field]
<input
name="email"
onBlur={() => setTouched(t => ({ ...t, email: true }))}
aria-invalid={!!showError('email')}
aria-describedby={showError('email') ? 'email-error' : undefined}
/>
{showError('email') && <p id="email-error" role="alert">{errors.email}</p>}
// aria-describedby is what makes a screen reader announce the error
// with the field. Without it the message is visually present and
// functionally invisible.Client Validation Is Not Security
What each layer is actually for.
// Client validation -> fast feedback, fewer round trips. A
// convenience, trivially bypassed with DevTools.
// Server validation -> the actual rule. Never optional.
// Database constraint -> the last line, for what must never be wrong.
// So the server can always reject, and the UI must handle it by
// mapping errors back onto fields:
const res = await fetch('/api/signup', { method: 'POST', body })
if (res.status === 422) {
const { errors } = await res.json() // { email: 'Already registered' }
setErrors(errors)
return // keep the user's input
}
// Errors only the server can know — an email already registered, a
// coupon already used, a slot just taken — will always arrive this
// way, so the mapping is required regardless of client validation.
// Write the error messages in the user's terms:
'password_policy_violation' // never show this
'Include at least one number' // show thisKey Points to Remember
- 1Define validation rules once as a schema, which in TypeScript also produces the values type
- 2Validate on blur, then on change once a field has already errored, and everything on submit
- 3Move focus to the first invalid field on submit, and link messages with aria-describedby
- 4Client validation is convenience only — the server rule is the real one and can always reject
- 5Map server 422 errors back onto individual fields and never clear the user's input on failure
Interview Questions
Sign in to ask AriaWhy validate on blur rather than on every keystroke?
How do you make a validation message accessible to a screen-reader user?
If the client already validates, why does the server still have to?
Ask Aria about Validation — Rules, Timing and Messages
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.