Capítulo 10: Formulários Avançados e Validação com Vee-Validate e Zod
Capítulo 10: Formulários Avançados e Validação com Vee-Validate e Zod
Especialização em Front-End com Vue • Vue 3 & Nuxt 3 • Composition API, Pinia, Vue Router, SSR e TypeScript
🗺️ Mapa Conceitual da Validação Baseada em Esquemas
flowchart TD
A["Contrato de Domínio: Schema Zod (z.object)"] --> B["toTypedSchema(@vee-validate/zod)"]
B --> C["useForm() do Vee-Validate 4"]
C --> D["defineField(): Vincula Inputs a Flags Reativas"]
C --> E["handleSubmit(): Validação Antes do Envio"]
D --> F["meta.valid / meta.dirty / errors"]
E -->|Válido| G["Chamada à API REST / Backend"]
E -->|Inválido| H["Exibição de Mensagens de Erro por Campo"]
G --> I["resetForm() ou Mapeamento de Erros da API"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style G fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style H fill:#fce4ec,stroke:#e91e63,stroke-width:2px
🏛️ 1. Arquitetura de Formulários Corporativos no Vue 3
Em sistemas corporativos, formulários lidam com dezenas de campos, regras de validação interdependentes, estados de carregamento e mensagens de erro do backend. Gerenciar esse ecossistema com múltiplos ref() manuais gera código frágil e duplicado.
A solução canônica no ecossistema Vue 3 é a combinação de Vee-Validate 4 (para gerenciamento do ciclo de vida, rastreamento de dirty/touched e submissões) com Zod (para validação estrita baseada em esquemas e inferência de tipos TypeScript).
1.1 O Princípio Schema-First com Zod
No paradigma Schema-First, toda regra de negócio do formulário é descrita em um único objeto Zod imutável e reutilizável:
import * as z from 'zod';
export const userRegistrationSchema = z.object({
fullName: z.string().min(3, 'Mínimo de 3 caracteres').max(60),
email: z.string().email('E-mail corporativo inválido'),
role: z.enum(['ADMIN', 'OPERATOR', 'VIEWER']),
salaryExpectation: z.number().positive('O valor deve ser positivo')
});
// Inferência automática do contrato de dados:
export type UserRegistrationDTO = z.infer<typeof userRegistrationSchema>;
Isso garante que a mesma regra possa ser utilizada tanto no frontend quanto em testes de integração e até mesmo em endpoints backend no Node.js/Nitro.
1.2 O Composable useForm e a Macro defineField
No Vee-Validate 4.10+, o método defineField substitui componentes wrappers verbosos (<Field> e <ErrorMessage>). Ele gera um par com a Ref do valor e um objeto de propriedades prontas para serem vinculadas com v-bind:
const [email, emailProps] = defineField('email');
// No template:
<input v-model="email" v-bind="emailProps" />
O emailProps escuta os eventos input e blur nativos para atualizar os estados meta.touched e meta.dirty sem necessidade de código manual.
1.3 A Máquina de Estados do Formulário: meta
O objeto meta do useForm expõe flags reativas fundamentais para UX profissional:
meta.valid: Indica se 100% dos campos atendem ao schema Zod.meta.dirty: Indica se o usuário alterou algum valor em relação aos dados originais (initialValues).meta.touched: Indica se o usuário já interagiu com o campo (saiu do foco viablur).isSubmitting: Flag booleana que se tornatruedurante a execução da Promise assíncrona dohandleSubmit.
⚖️ 2. Tabela Comparativa: Validação Manual vs Vee-Validate + Zod
| Característica | Validação Manual com ref() |
Vee-Validate 4 + Zod |
|---|---|---|
| Linhas de Código de Boilerplate | Elevadíssimo; múltiplos ternários e flags booleanas. | Mínimo; schema declarativo centralizado. |
| Segurança de Tipos (TypeScript) | Manual; suscetível a erros de digitação de campos. | Automatizada com z.infer<typeof schema>. |
| Rastreamento de Estados (Dirty/Touched) | Muito complexo de implementar manualmente. | Nativo através do objeto reativo meta. |
| Prevenção de Múltiplos Cliques | Exige criação manual de flags isSubmitting. |
Automática durante a execução de handleSubmit. |
| Reaproveitamento de Regras | Baixo; regras acopladas ao componente SFC. | Alto; schemas Zod podem ser compartilhados entre pacotes. |
⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)
Anti-Pattern 1: Disparar Erros Antes do Usuário Interagir (Aggressive Validation)
Erro Comum: Mostrar mensagens de erro em vermelho logo na primeira renderização da página, antes do usuário tocar no campo.
Abordagem Correta: Condicione a exibição visual do erro à flag meta.touched ou utilize a validação padrão ao submeter.
Anti-Pattern 2: Esquecer de Chamar toTypedSchema()
Erro Comum: Passar o schema Zod cru diretamente para o Vee-Validate (validationSchema: meuSchemaZod).
Abordagem Correta: O Vee-Validate é agnóstico a bibliotecas de schema. Use sempre o adaptador oficial: validationSchema: toTypedSchema(meuSchemaZod).
💻 4. Código Conceitual Arquitetural Comentado
<!-- src/components/CreateProjectForm.vue -->
<script setup lang="ts">
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import * as z from 'zod';
const projectSchema = z.object({
title: z.string().min(5, 'Título deve possuir pelo menos 5 caracteres'),
budget: z.number({ invalid_type_error: 'Informe um valor numérico' }).min(1000, 'Orçamento mínimo de R$ 1.000'),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH'])
});
type ProjectDTO = z.infer<typeof projectSchema>;
const { defineField, handleSubmit, errors, isSubmitting, meta, resetForm } = useForm<ProjectDTO>({
validationSchema: toTypedSchema(projectSchema),
initialValues: { title: '', budget: 5000, priority: 'MEDIUM' }
});
const [title, titleProps] = defineField('title');
const [budget, budgetProps] = defineField('budget');
const [priority, priorityProps] = defineField('priority');
const onSubmit = handleSubmit(async (values) => {
// Simulação de chamada de API backend
await new Promise((resolve) => setTimeout(resolve, 800));
console.log('[API] Projeto criado com sucesso:', values);
resetForm();
});
</script>
<template>
<form @submit="onSubmit" class="max-w-md mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-4">
<h3 class="font-bold text-slate-800 text-lg">Novo Projeto Corporativo</h3>
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">Título do Projeto:</label>
<input
type="text"
v-model="title"
v-bind="titleProps"
class="w-full border px-3 py-2 rounded-lg text-sm"
:class="errors.title ? 'border-red-500 bg-red-50' : 'border-slate-300'"
/>
<span v-if="errors.title" class="text-xs text-red-600 mt-1 block">{{ errors.title }}</span>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">Orçamento (R$):</label>
<input
type="number"
v-model.number="budget"
v-bind="budgetProps"
class="w-full border px-3 py-2 rounded-lg text-sm"
/>
<span v-if="errors.budget" class="text-xs text-red-600 mt-1 block">{{ errors.budget }}</span>
</div>
<button
type="submit"
:disabled="isSubmitting || !meta.valid"
class="w-full py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-xs font-bold transition-colors disabled:opacity-50"
>
{{ isSubmitting ? 'Salvando...' : 'Cadastrar Projeto' }}
</button>
</form>
</template>
🛠️ 5. Cheat-Sheet do Vee-Validate + Zod
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import * as z from 'zod';
// 1. Definição do Schema
const schema = z.object({
email: z.string().email(),
idade: z.number().min(18)
});
// 2. Inicialização do Form
const { defineField, handleSubmit, errors, meta, resetForm } = useForm({
validationSchema: toTypedSchema(schema)
});
// 3. Binding de Campo
const [email, emailAttrs] = defineField('email');
// 4. Handler com Validação Embutida
const salvar = handleSubmit((values) => {
// Executado apenas se for 100% válido
});