VeeValidate Architecture: Schema-First Validation in Vue 3 with Yup or Zod
VeeValidate v4 uses schema-first validation where Yup/Zod schemas become the single source of truth. Learn the minimal design, trust boundaries, and failure modes for robust form validation in Vue 3.
01 Jan 2026, 23:26 UTC

The Core Problem: Validation Logic Scattered Across Components
When form validation rules live in components alongside template markup, you lose the ability to maintain, test, and reuse validation logic. VeeValidate v4 solves this with a schema-first approach where Yup, Zod, or Valibot schemas become the single source of truth for validation rules and error messages.
Minimal Viable Design: Schema + useForm + useField
The smallest complete design requires three pieces working together:
- Schema definition - Declare validation rules and messages in a separate schema file
- useForm composable - Initialize form state with your validation schema
- useField or Field component - Bind individual inputs to the validation system
Example: Login Form with Yup Schema
import * as yup from 'yup';
import { useForm, useField } from 'vee-validate';
// Schema is the single source of truth
const schema = yup.object({
email: yup.string()
.email('Must be a valid email')
.required('Email is required'),
password: yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Password is required')
});
export default {
setup() {
const { handleSubmit, formState } = useForm({
validationSchema: schema,
validateOnMount: true,
validateOnChange: true
});
const { value: email, errorMessage: emailError } = useField('email');
const { value: password, errorMessage: passwordError } = useField('password');
const onSubmit = handleSubmit((values) => {
// formState.isValid is reactive and accurate
console.log('Submitting:', values);
});
return { onSubmit, email, emailError, password, passwordError, formState };
}
};
Trust Boundaries: Schema vs UI Responsibility
The schema owns all validation logic and error messages. The UI layer only renders state and collects input. Never duplicate rules in components - doing so creates maintenance nightmares and inconsistent user experiences.
Client-server boundary: Schema validation runs in the browser and can be bypassed. Your API must always re-validate on the server, treating client validation as a UX convenience, not a security measure.
Validation timing: Use validateOnMount, validateOnChange, and validateOnBlur props to control when the schema executes. For example, validateOnBlur: true gives users immediate feedback when they leave a field, while validateOnModelUpdate: false prevents validation during typing.
Operational Validation Checks
VeeValidate exposes reactive refs for all form state you need:
formState.isValid- Reactive boolean; use to gate API callsformState.errors- Object mapping field names to error messagesformState.dirty- True if any field differs from initial valueformState.touched- True if any field has been blurred
Server Error Mapping Example
const onSubmit = handleSubmit(async (values) => {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(values)
});
if (!response.ok) {
const error = await response.json();
// Map server errors to form fields
if (error.fieldErrors) {
setErrors(error.fieldErrors);
}
return;
}
// Success handling
} catch (err) {
setFieldError('general', 'An unexpected error occurred');
}
});
Failure Modes and Their Triggers
Understanding when things break helps you write more resilient code:
- Schema parsing errors - Invalid schema configuration crashes during setup. Test schemas in isolation.
- Async validator exceptions - Throwing instead of returning
falseor rejecting a promise breaks submission flow. Always return a boolean or promise. - Stale initialValues - Setting
initialValuesafter component mount causes dirty state mismatches. - Missing name attributes -
<Field name="email" />without a name breaks field registration entirely. - Circular references - Schemas with circular dependencies cause stack overflows during validation.
When to Change the Design
The current design assumes a client-side schema. Consider alternatives when:
- Bundle size matters - Yup adds ~30kb gzipped. Switch to Zod (~8kb) or Valibot (~1kb) for size-sensitive applications.
- Cross-field dependencies - Use Yup's
.when()or Zod's.refine()for conditional validation logic. - Server-driven forms - Dynamic schemas loaded from API require runtime schema compilation.
- Zero-dependency needs - Consider TanStack Form or native HTML constraint validation.
Verification Checklist
Before shipping, verify these behaviors:
- Create a minimal Vue 3 + Vite project and implement the login form example
- Type an invalid email, blur the field, confirm error appears without submission
- Click submit with invalid form -
onSubmitshould not fire;formState.isValidstays false - Simulate server error mapping with
setErrors({ email: 'Server says taken' })and confirm UI updates - Check bundle size impact with
npm run buildand inspect chunk sizes - Test field arrays with add/remove/reorder operations to confirm validation follows each item
Key Takeaways
- Schema is the single source of truth - never duplicate rules in components
- Always re-validate on the server - client validation is UX, not security
- Use
formState.isValidreactively to gate API calls - Choose schema library based on bundle size: Valibot for minimal, Zod for balance, Yup for features
- Decompose reactive objects with
toRefsor access viaformState.value
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.