patterntypescriptreactModerate
Server-side validation — reconciling API errors with form fields
Viewed 0 times
setErrorserver validationAPI errorsinline form errorsreact hook form server errorroot errorfield error mapping
Problem
Client-side validation cannot catch business-rule errors like 'email already registered' or 'username taken'. When the server returns field-specific errors, they must be mapped back to the form fields so users see them inline rather than as a generic toast.
Solution
Use setError from React Hook Form to display server-returned field errors:
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
email: z.string().email(),
username: z.string().min(3),
});
type FormData = z.infer<typeof schema>;
// Expected API error shape
interface ApiError {
field: keyof FormData | 'root';
message: string;
}
function RegisterForm() {
const { register, handleSubmit, setError, formState: { errors } } =
useForm<FormData>({ resolver: zodResolver(schema) });
const onSubmit = async (data: FormData) => {
try {
await registerUser(data);
} catch (err: unknown) {
const apiErrors = (err as { errors: ApiError[] }).errors;
apiErrors.forEach(({ field, message }) => {
setError(field, { type: 'server', message });
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('username')} />
{errors.username && <p>{errors.username.message}</p>}
{/ Root-level error (non-field errors) /}
{errors.root && <p role="alert">{errors.root.message}</p>}
<button type="submit">Register</button>
</form>
);
}
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
email: z.string().email(),
username: z.string().min(3),
});
type FormData = z.infer<typeof schema>;
// Expected API error shape
interface ApiError {
field: keyof FormData | 'root';
message: string;
}
function RegisterForm() {
const { register, handleSubmit, setError, formState: { errors } } =
useForm<FormData>({ resolver: zodResolver(schema) });
const onSubmit = async (data: FormData) => {
try {
await registerUser(data);
} catch (err: unknown) {
const apiErrors = (err as { errors: ApiError[] }).errors;
apiErrors.forEach(({ field, message }) => {
setError(field, { type: 'server', message });
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('username')} />
{errors.username && <p>{errors.username.message}</p>}
{/ Root-level error (non-field errors) /}
{errors.root && <p role="alert">{errors.root.message}</p>}
<button type="submit">Register</button>
</form>
);
}
Why
setError injects an error into the form state for a specific field without triggering re-validation against the schema. The 'server' type distinguishes API errors from client validation errors, and they are cleared automatically when the user edits the field.
Gotchas
- setError with type: 'server' does not prevent form submission on the next submit — the error is cleared when the field value changes
- Use setError('root', ...) for errors that are not tied to a specific field (e.g. rate limit exceeded)
- Server errors set with setError are cleared on the next successful handleSubmit — no need to manually clear them
- If the API returns errors in a different shape, normalise them before calling setError — create a helper that maps the API format to { field, message }
Revisions (0)
No revisions yet.