Centralizing Vue Form Logic with VeeValidate and Zod
Stop scattering validation rules across your Vue templates. Learn how to use VeeValidate with Zod to create a single, type-safe source of truth for your form logic.
09 Mar 2026, 04:40 UTC

The Problem: Fragmented Validation Logic
In many Vue applications, validation logic ends up scattered. You might have some rules defined as strings in the template, some as manual if/else blocks in a submit function, and others as TypeScript interfaces that only exist for the developer, not the runtime. This fragmentation makes it difficult to maintain consistency, especially when a field's requirements change or when you need to share those same rules with a Node.js backend.
The most effective way to solve this is to move the source of truth out of the UI components and into a validation schema. By integrating VeeValidate with Zod, you can define a single, type-safe schema that handles both the runtime validation and the TypeScript typing for your form data.
Why Use a Schema-Based Approach?
VeeValidate allows you to use a validationSchema property within its useForm hook. Instead of attaching individual rules to every Field component, the form looks at the schema to determine if a value is valid. This provides several engineering advantages:
- Cross-Field Dependencies: Validating that a "Password" and "Confirm Password" field match is trivial in a schema, whereas it is cumbersome when rules are isolated to individual fields.
- Type Safety: Zod allows you to infer the TypeScript type directly from the schema, ensuring that the data you send to your API matches the data you validated.
- Decoupling: Your validation logic is a plain JavaScript object. You can move it to a separate file, test it in isolation with Vitest or Jest, and import it into multiple components.
Implementation Example: User Registration
To implement this, you need veevalidate and zod, along with the @veevalidate/zod resolver. This example assumes a Vue 3 project using the Composition API.
import { useForm } from 'veevalidate';
import { toTypedSchema } from '@veevalidate/zod';
import * as zod from 'zod';
// 1. Define the schema outside the component
const registrationSchema = zod.object({
email: zod.string().email('Invalid email format'),
password: zod.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: zod.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'], // Set the error to the confirmPassword field
});
// 2. Use the schema in the component
export default {
setup() {
const { errors, defineField, handleSubmit } = useForm({
validationSchema: toTypedSchema(registrationSchema),
});
// defineField creates a model and a binding for the input
const [email, emailProps] = defineField('email');
const [password, passwordProps] = defineField('password');
const [confirmPassword, confirmPasswordProps] = defineField('confirmPassword');
const onSubmit = handleSubmit((values) => {
console.log('Form submitted successfully:', values);
});
return { email, emailProps, password, passwordProps, confirmPassword, confirmPasswordProps, errors, onSubmit };
}
};
Execution Details
- Where to run: These commands and logic reside in your Vue component
<script setup>orsetup()function. - Permissions: Standard npm/yarn install permissions for
veevalidate,zod, and@veevalidate/zod. - Expected Check: When the user types an email that lacks an "@" symbol, the
errors.emailobject should automatically populate with "Invalid email format" without manual triggers.
Trade-offs and Limitations
While schema-based validation is powerful, it introduces a few considerations:
| Consideration | Impact | Mitigation |
|---|---|---|
| Performance | Slight overhead on very large forms (100+ fields) due to schema parsing. | Use validateOnBlur instead of validateOnChange to reduce trigger frequency. |
| Type Casting | HTML inputs always return strings, but Zod may expect numbers. | Use zod.coerce.number() to automatically cast string inputs to numbers. |
| Bundle Size | Adding Zod increases the client-side bundle size. | Only a concern for extremely size-constrained environments; generally negligible for most apps. |
Verifying the Result
To ensure your schema is working as intended, perform these three checks:
- Trigger a specific rule: Enter a 4-character password and verify the "at least 8 characters" message appears.
- Test cross-field logic: Enter two different passwords and verify the
confirmPasswordfield displays the "Passwords don't match" error. - Verify submission block: Attempt to click the submit button with empty fields; the
onSubmithandler should not execute, and all required fields should display errors.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.