Form Validation (Client + Server)
Validate user input on the client for fast feedback and on the server for security and a single source of truth.
The problem I keep seeing
Forms need validation: required fields, format (email, phone), length, and business rules (e.g. “email not already taken”). If you only validate on the server, the user gets errors after a round-trip and a full submit. If you only validate on the client, you’re not secure—anyone can bypass the client. You want both: client-side for UX, server-side for correctness and security, with one schema if possible so rules don’t drift.
The other trap: showing errors too early (on first keystroke) is noisy; showing them only on submit is late. A good default is validate on blur and on submit, and optionally on change after the field has been touched.
Naive approach
Validate only in handleSubmit. Set error state and show messages next to fields. No server-side validation yet; no feedback until the user submits.
First improvement
Introduce a schema (e.g. Zod) and validate the same object on blur and on submit. Track “touched” so you don’t show errors before the user has left the field. Reuse the schema so client and server can share one definition (e.g. in a shared package).
Why this helps: User sees errors when they leave the field, not only after submit. One schema reduces drift between client and server.
Remaining issues
- Re-renders: Storing every field in React state and validating on every blur/change can re-render the whole form. Libraries like React Hook Form keep field state in refs and only re-render when needed.
- Server errors: After submit, the server may return field-level errors (e.g. “Email already taken”). You need to map those back to fields and set error state.
- Type safety: Form data and error types should be derived from the schema so you don’t repeat yourself.
Production pattern
Use React Hook Form with a Zod resolver. One schema, type-safe form data, minimal re-renders. On submit, send data to the server; if the server returns 400 with field errors, use setError to show them. On the server, validate again with the same (or shared) schema and return structured errors so the client can display them per field.
When I use this
- Use: Any form that collects user input you’ll send to the server: signup, login, settings, checkout. Always validate on the server; add client validation for UX.
- Mode:
onBluroronTouchedfor most forms;onChangeonly if you need live feedback (e.g. password strength). - Skip client validation only when: The form is internal or the only validation is server-only (e.g. auth token). Even then, server must validate.
Security
Gotchas
- setError for server errors: After a failed submit, call
setError('fieldName', { message: serverMessage })(orsetError('root', ...)for a global message) so the user sees why the request failed. - Dependent fields: If “confirm password” must match “password”, validate in the schema with
refineor a custom check so both fields show the error when they don’t match. - Accessibility: Use
aria-invalidandrole="alert"for error messages so screen readers announce them. Link the message to the input witharia-describedby.