20 de mar. de 2025·7 min de leitura

Arquitetura de formulários Vue 3 para apps empresariais: padrões reutilizáveis

Arquitetura de formulários Vue 3 para apps empresariais: componentes de campo reutilizáveis, regras de validação claras e formas práticas de mostrar erros do servidor em cada input.

Arquitetura de formulários Vue 3 para apps empresariais: padrões reutilizáveis

Por que o código de formulários quebra em apps empresariais reais

Um formulário em um app empresarial raramente fica pequeno. Começa como "apenas alguns campos" e vai crescendo até dezenas de campos, seções condicionais, permissões e regras que precisam se manter em sincronia com a lógica do backend. Após algumas mudanças de produto, o formulário ainda funciona, mas o código fica frágil.

A arquitetura de formulários no Vue 3 importa porque é onde os "remendos rápidos" se acumulam: mais um watcher, mais um caso especial, mais um componente copiado. Hoje funciona, mas fica difícil confiar e difícil de alterar.

Os sinais de aviso são familiares: comportamento de input repetido entre páginas (rótulos, formatação, marcação de obrigatório, dicas), posicionamento inconsistente de erros, regras de validação espalhadas por componentes e erros do backend reduzidos a um toast genérico que não diz ao usuário o que corrigir.

Essas inconsistências não são só questões de estilo de código. Viram problemas de UX: pessoas reenviam formulários, tickets de suporte aumentam e equipes evitam mexer em formulários com medo de quebrar um caso de borda oculto.

Uma boa configuração torna formulários entediantes no melhor sentido. Com uma estrutura previsível, você pode adicionar campos, mudar regras e tratar respostas do servidor sem retrabalhar tudo.

Você quer um sistema de formulários que entregue reuso (um campo se comporta igual em qualquer lugar), clareza (regras e tratamento de erros fáceis de revisar), comportamento previsível (touched, dirty, reset, submit) e feedback melhor (erros do servidor aparecem nos inputs exatos que precisam de atenção). Os padrões abaixo focam em componentes de campo reutilizáveis, validação legível e mapeamento de erros do servidor para inputs específicos.

Um modelo mental simples para a estrutura do formulário

Um formulário que se sustenta ao longo do tempo é um pequeno sistema com partes claras, não uma pilha de inputs.

Pense em quatro camadas que se comunicam em uma direção: a UI coleta entrada, o estado do formulário a armazena, a validação explica o que está errado e a camada de API carrega e salva.

As quatro camadas (e o que cada uma assume)

  • Componente de campo (Field UI): renderiza o input, rótulo, dica e texto de erro. Emite alterações de valor.
  • Estado do formulário: guarda valores e erros (além de flags touched e dirty).
  • Regras de validação: funções puras que leem valores e retornam mensagens de erro.
  • Chamadas de API: carregam dados iniciais, submetem mudanças e traduzem respostas do servidor em erros de campo.

Essa separação mantém as mudanças contidas. Quando surge um novo requisito, você atualiza uma camada sem quebrar as outras.

O que pertence a um campo vs ao formulário pai

Um componente de campo reutilizável deve ser sem graça. Não deve conhecer sua API, modelo de dados ou regras de validação. Deve apenas exibir um valor e mostrar um erro.

O formulário pai coordena o resto: quais campos existem, onde vivem os valores, quando validar e como submeter.

Uma regra simples ajuda: se a lógica depende de outros campos (por exemplo, "Estado" é obrigatório somente quando "País" é Brasil), mantenha-a no formulário pai ou na camada de validação, não dentro do componente de campo.

Quando adicionar um novo campo for realmente pouco trabalhoso, você geralmente só mexe nos defaults ou no schema, na marcação onde o campo é colocado e nas regras de validação do campo. Se adicionar um input força mudanças em componentes não relacionados, seus limites estão borrados.

Componentes de campo reutilizáveis: o que padronizar

Quando os formulários crescem, a vitória mais rápida é parar de construir cada input como se fosse único. Componentes de campo devem ser previsíveis. Isso os torna rápidos de usar e fáceis de revisar.

Um conjunto prático de blocos de construção:

  • BaseField: wrapper para rótulo, dica, texto de erro, espaçamento e atributos de acessibilidade.
  • Componentes de input: TextInput, SelectInput, DateInput, Checkbox, etc. Cada um foca no controle.
  • FormSection: agrupa campos relacionados com um título, ajuda curta e espaçamento consistente.

Para props, mantenha um conjunto pequeno e aplique em todos os lugares. Trocar o nome de uma prop em 40 formulários é doloroso.

Isso geralmente compensa imediatamente:

  • modelValue e update:modelValue para v-model
  • label
  • required
  • disabled
  • error (mensagem única, ou um array se preferir)
  • hint

Slots são onde você permite flexibilidade sem quebrar a consistência. Mantenha o layout do BaseField estável, mas permita pequenas variações como uma ação à direita ("Enviar código") ou um ícone à esquerda. Se uma variação aparecer duas vezes, faça um slot em vez de bifurcar o componente.

Padronize a ordem de renderização (rótulo, controle, dica, erro). Usuários escaneiam mais rápido, testes ficam mais simples e o mapeamento de erros do servidor fica direto porque cada campo tem um lugar óbvio para exibir mensagens.

Estado do formulário: values, touched, dirty e reset

A maioria dos bugs de formulário em apps empresariais não vem dos inputs. Vem do estado espalhado: valores em um lugar, erros em outro e um botão de reset que só funciona pela metade. Uma arquitetura de formulário Vue 3 limpa começa com uma forma de estado consistente.

Primeiro, escolha um esquema de nomes para as chaves dos campos e mantenha-o. A regra mais simples: a chave do campo é igual à chave do payload da API. Se o servidor espera first_name, sua chave de formulário deve ser first_name também. Essa pequena escolha facilita validação, salvamento e mapeamento de erros do servidor.

Mantenha o estado do formulário em um só lugar (um composable, um store Pinia ou um componente pai) e faça cada campo ler e escrever através desse estado. Uma estrutura plana funciona para a maioria das telas. Só use aninhamento quando sua API for realmente aninhada.

const state = reactive({
  values: { first_name: '', last_name: '', email: '' },
  touched: { first_name: false, last_name: false, email: false },
  dirty: { first_name: false, last_name: false, email: false },
  errors: { first_name: '', last_name: '', email: '' },
  defaults: { first_name: '', last_name: '', email: '' }
})

Uma forma prática de pensar nas flags:

  • touched: o usuário já interagiu com esse campo?
  • dirty: o valor é diferente do default (ou do último salvo)?
  • errors: qual mensagem o usuário deve ver agora?
  • defaults: para o que fazemos reset?

O comportamento de reset deve ser previsível. Ao carregar um registro existente, defina tanto values quanto defaults a partir da mesma fonte. Então reset() pode copiar defaults de volta para values, limpar touched, limpar dirty e limpar errors.

Exemplo: um formulário de perfil de cliente carrega email do servidor. Se o usuário editá-lo, dirty.email vira true. Se ele clicar em Reset, o email volta ao valor carregado (não para uma string vazia) e a tela fica limpa novamente.

Regras de validação que permanecem legíveis

Mantenha a validação legível
Centralize regras e fluxos para que mudanças de produto não espalhem correções por componentes.
Experimente agora

Validação legível é menos sobre a biblioteca e mais sobre como você expressa regras. Se você consegue olhar para um campo e entender suas regras em alguns segundos, o código do formulário continua mantível.

Escolha um estilo de regras que você consiga manter

A maioria das equipes se encaixa em uma destas abordagens:

  • Regras por campo: as regras vivem perto do uso do campo. Fácil de escanear, ótimo para formulários pequenos a médios.
  • Regras por schema: as regras vivem em um objeto ou arquivo central. Ótimo quando muitas telas reutilizam o mesmo modelo.
  • Híbrido: regras simples perto dos campos, regras complexas ou compartilhadas em um schema central.

Qualquer que seja sua escolha, mantenha nomes de regras e mensagens previsíveis. Algumas regras comuns (required, length, format, range) valem mais que uma longa lista de helpers pontuais.

Escreva regras como inglês simples

Uma boa regra lê como uma sentença: "Email é obrigatório e deve parecer um email." Evite linhas inteligentes que escondem intenção.

Para a maioria dos formulários empresariais, retornar uma mensagem por campo por vez (a primeira falha) mantém a UI calma e ajuda usuários a corrigirem mais rápido.

Regras comuns e amigáveis:

  • Required somente quando o usuário realmente precisa preencher o campo.
  • Length com números reais (por exemplo, 2 a 50 caracteres).
  • Format para email, telefone, CEP, sem regex excessivamente restritiva que rejeite entradas reais.
  • Range como "data não pode ser futura" ou "quantidade entre 1 e 999."

Torne checagens assíncronas óbvias

Validação assíncrona (como "nome de usuário já existe") fica confusa se disparar silenciosamente.

Dispare checagens no blur ou após uma pequena pausa, mostre um estado claro de "Verificando..." e cancele ou ignore requisições antigas quando o usuário continuar digitando.

Decida quando a validação roda

O timing importa tanto quanto as regras. Uma configuração amigável ao usuário costuma ser:

  • On change para campos que se beneficiam de feedback ao vivo (como força da senha), mas mantenha comedida.
  • On blur para a maioria dos campos, assim usuários podem digitar sem erros constantes.
  • On submit para o formulário completo como a rede de segurança final.

Mapear erros do servidor para o input correto

Adicione integrações sem caos
Conecte auth, pagamentos, mensagens e integrações de IA sem fio manual para cada caso de borda.
Experimente AppMaster

Checagens do lado do cliente são só metade da história. Em apps empresariais, o servidor rejeita salvamentos por regras que o navegador não conhece: duplicatas, checagens de permissão, dados obsoletos, mudanças de estado e mais. Boa UX de formulário depende de transformar essa resposta em mensagens claras ao lado dos inputs certos.

Normalize erros em uma única forma interna

Backends raramente concordam no formato de erro. Alguns retornam um objeto único, outros listas, outros mapas aninhados indexados por nome de campo. Converta qualquer coisa que você receba em uma única forma interna que seu formulário consiga renderizar.

// what your form code consumes
{
  fieldErrors: { "email": ["Already taken"], "address.street": ["Required"] },
  formErrors: ["You do not have permission to edit this customer"]
}

Mantenha algumas regras consistentes:

  • Armazene erros de campo como arrays (mesmo que haja só uma mensagem).
  • Converta diferentes estilos de caminho para um só estilo (caminhos com ponto funcionam bem: address.street).
  • Mantenha erros não relacionados a campo separadamente como formErrors.
  • Guarde o payload bruto do servidor para log, mas não o renderize.

Mapeie paths do servidor para suas chaves de campo

A parte complicada é alinhar a ideia do servidor de "path" com as chaves do seu formulário. Decida a chave de cada campo de componente (por exemplo, email, profile.phone, contacts.0.type) e mantenha-a.

Então escreva um pequeno mapper que trate os casos comuns:

  • address.street (notação ponto)
  • address[0].street (colchetes para arrays)
  • /address/street (estilo JSON Pointer)

Após normalizar, \u003cField name=\"address.street\" /\u003e deve ser capaz de ler fieldErrors[\"address.street\"] sem casos especiais.

Suporte aliases quando necessário. Se o backend retornar customer_email mas sua UI usar email, mantenha um mapeamento como { customer_email: "email" } durante a normalização.

Erros de campo, erros de formulário e foco

Nem todo erro pertence a um único input. Se o servidor disser "Limite do plano atingido" ou "Pagamento necessário", mostre isso acima do formulário como uma mensagem de nível de formulário.

Para erros específicos de campo, mostre a mensagem ao lado do input e direcione o usuário para o primeiro problema:

  • Após definir os erros do servidor, encontre a primeira chave em fieldErrors que exista no formulário renderizado.
  • Role até ela e foque-a (usando uma ref por campo e nextTick).
  • Limpe erros do servidor de um campo quando o usuário editar esse campo novamente.

Passo a passo: juntando a arquitetura

Formulários ficam calmos quando você decide cedo o que pertence ao estado do formulário, UI, validação e API, e então os conecta com algumas funções pequenas.

Uma sequência que funciona para a maioria dos apps empresariais:

  • Comece com um modelo de formulário e chaves de campo estáveis. Essas chaves viram o contrato entre componentes, validadores e erros do servidor.
  • Crie um wrapper BaseField para rótulo, texto de ajuda, marcação de obrigatório e exibição de erro. Mantenha componentes de input pequenos e consistentes.
  • Adicione uma camada de validação que possa rodar por campo e validar tudo no submit.
  • Submeta para a API. Se falhar, traduza erros do servidor em { [fieldKey]: message } para que o input certo mostre a mensagem certa.
  • Mantenha o tratamento de sucesso separado (reset, toast, navegação) para que não vaze para componentes e validadores.

Um ponto de partida simples para estado:

const values = reactive({ email: '', name: '', phone: '' })
const touched = reactive({ email: false, name: false, phone: false })
const errors = reactive({}) // { email: '...', name: '...' }

Seu BaseField recebe label, error e talvez touched, e renderiza a mensagem em um só lugar. Cada componente de input só se preocupa em bindar e emitir updates.

Para validação, mantenha regras perto do modelo usando as mesmas chaves:

const rules = {
  email: v => (!v ? 'Email is required' : /@/.test(v) ? '' : 'Enter a valid email'),
  name: v => (v.length < 2 ? 'Name is too short' : ''),
}

function validateAll() {
  Object.keys(rules).forEach(k => {
    const msg = rules[k](values[k])
    if (msg) errors[k] = msg
    else delete errors[k]
    touched[k] = true
  })
  return Object.keys(errors).length === 0
}

Quando o servidor responder com erros, mapeie-os usando as mesmas chaves. Se a API retornar { "field": "email", "message": "Already taken" }, defina errors.email = 'Already taken' e marque como touched. Se o erro for global (como "permissão negada"), mostre-o acima do formulário.

Cenário exemplo: editando um perfil de cliente

Coloque a lógica em um só lugar
Use processos de negócio drag-and-drop para manter a lógica de formulários clara e fácil de alterar.
Construir fluxos

Imagine uma tela interna de administração onde um agente de suporte edita o perfil de um cliente. O formulário tem quatro campos: nome, email, telefone e role (Customer, Manager, Admin). É pequeno, mas mostra os problemas comuns.

Regras do lado cliente devem ser claras:

  • Nome: obrigatório, comprimento mínimo.
  • Email: obrigatório, formato de email válido.
  • Telefone: opcional, mas se preenchido deve seguir o formato aceito.
  • Role: obrigatório, e às vezes condicional (somente usuários com permissão podem atribuir Admin).

Um contrato de componente consistente ajuda: cada campo recebe o valor atual, o texto de erro atual (se houver) e alguns booleanos como touched e disabled. Rótulos, marcação de obrigatório, espaçamento e estilo de erro não devem ser reinventados em cada tela.

Agora o fluxo de UX. O agente edita o email, pressiona Tab e vê uma mensagem inline se o formato estiver errado. Corrige, clica em Salvar e o servidor responde:

  • email já existe: mostrar sob Email e focar esse campo.
  • telefone inválido: mostrar sob Telefone.
  • permissão negada: mostrar uma mensagem de nível de formulário no topo.

Se você mantiver erros indexados pelo nome do campo (email, phone, role), o mapeamento é simples. Erros de campo vão para os inputs; erros de formulário vão para uma área dedicada.

Erros comuns e como evitá-los

Lance onde sua equipe roda
Faça deploy no AppMaster Cloud ou na sua própria infraestrutura AWS, Azure ou Google Cloud.
Implantar App

Mantenha a lógica em um só lugar

Copiar regras de validação em cada tela parece rápido até as políticas mudarem (regras de senha, IDs fiscais obrigatórios, domínios permitidos). Centralize regras (schema, arquivo de regras, função compartilhada) e faça os formulários consumirem o mesmo conjunto.

Também evite deixar inputs de baixo nível fazerem demais. Se seu <TextField> sabe chamar a API, tentar retry em falhas e parsear payloads de erro do servidor, ele deixa de ser reutilizável. Componentes de campo devem renderizar, emitir mudanças de valor e exibir erros. Coloque chamadas de API e lógica de mapeamento no container do formulário ou em um composable.

Sintomas de mistura de responsabilidades:

  • A mesma mensagem de validação é escrita em vários lugares.
  • Um componente de campo importa um cliente de API.
  • Mudar um endpoint quebra vários formulários não relacionados.
  • Testes exigem montar metade do app só para checar um input.

Armadilhas de UX e acessibilidade

Um banner único como "Algo deu errado" não é suficiente. Pessoas precisam saber qual campo está errado e o que fazer em seguida. Use banners para falhas globais (rede, permissão negada) e mapeie erros do servidor para inputs específicos para que usuários possam agir rapidamente.

Problemas de loading e double-submit criam estados confusos. Ao submeter, desative o botão de envio, desative campos que não deveriam mudar durante o salvamento e mostre um estado claro de carregamento. Garanta que reset e cancelar restaurem o formulário corretamente.

Basicos de acessibilidade são fáceis de pular com componentes customizados. Algumas escolhas evitam dor real:

  • Todo input tem um rótulo visível (não só placeholder).
  • Erros estão conectados aos campos com atributos aria adequados.
  • O foco vai para o primeiro campo inválido após o submit.
  • Campos desativados são realmente não interativos e anunciados corretamente.
  • Navegação por teclado funciona de ponta a ponta.

Checklist rápido e próximos passos

Antes de liberar um novo formulário, rode um checklist rápido. Ele pega as pequenas lacunas que viram tickets de suporte depois.

  • Cada campo tem uma chave estável que corresponde ao payload e à resposta do servidor (incluindo caminhos aninhados como billing.address.zip)?
  • Você consegue renderizar qualquer campo usando uma API consistente de componente de campo (valor entra, eventos saem, erro e dica entram)?
  • No submit, você valida uma vez, bloqueia double submits e foca o primeiro campo inválido para que o usuário saiba por onde começar?
  • Você consegue mostrar erros no lugar certo: por campo (ao lado do input) e a nível de formulário (mensagem geral quando necessário)?
  • Após sucesso, você reseta o estado corretamente (values, touched, dirty) para que a próxima edição comece limpa?

Se alguma resposta for "não", conserte isso primeiro. A dor mais comum em formulários é descompasso: nomes de campo divergem da API, ou erros do servidor voltam em um formato que sua UI não consegue posicionar.

Se você está construindo ferramentas internas e quer acelerar, AppMaster (appmaster.io) segue os mesmos fundamentos: mantenha a UI de campo consistente, centralize regras e fluxos, e faça respostas do servidor aparecerem onde os usuários podem agir.

FAQ

Quando devo parar de criar inputs pontuais e migrar para componentes de campo reutilizáveis?

Padronize quando você perceber os mesmos rótulos, dicas, marcação de obrigatório, espaçamento e estilo de erro repetidos em várias telas. Se uma mudança “pequena” exige editar muitos arquivos, um wrapper BaseField compartilhado e alguns componentes de entrada consistentes economizam tempo rapidamente.

Que lógica deve viver dentro de um componente de campo vs no formulário pai?

Mantenha o componente de campo ‘burro’: ele renderiza o rótulo, o controle, a dica e o erro, e emite atualizações de valor. Coloque lógica entre campos, regras condicionais e tudo que depende de outros valores no formulário pai ou na camada de validação para que o campo continue reutilizável.

Como escolho chaves de campo para que a validação e o mapeamento de erros do servidor fiquem simples?

Use chaves estáveis que por padrão batam com o payload da API, como first_name ou billing.address.zip. Isso facilita validação e mapeamento de erros do servidor porque você não precisa traduzir nomes entre camadas.

Qual estado de formulário eu realmente preciso (values, touched, dirty, defaults)?

Um padrão simples é um objeto de estado que contenha values, errors, touched, dirty e defaults. Quando tudo lê e escreve através da mesma estrutura, o comportamento de reset e submit fica previsível e você evita bugs de “reset parcial”.

Qual é a maneira mais limpa de implementar Reset em um formulário de edição?

Ao carregar dados para edição, defina tanto values quanto defaults a partir da mesma resposta. Então reset() deve copiar defaults para values e limpar touched, dirty e errors, deixando a UI limpa e igual ao que o servidor retornou por último.

Como manter as regras de validação legíveis conforme o formulário cresce?

Comece com regras simples como funções indexadas pelas mesmas chaves do formulário. Retorne uma mensagem clara por campo (a primeira falha) para manter a UI calma e ajudar usuários a saber o que corrigir primeiro.

Quando devo validar: ao mudar, ao perder foco ou ao submeter?

Valide a maioria dos campos no evento blur e valide tudo no submit como checagem final. Use validação on-change apenas onde realmente ajuda (por exemplo, força da senha) para não punir o usuário enquanto ele digita.

Como lidar com validação assíncrona como “email já existe” sem irritar os usuários?

Faça checagens assíncronas no blur ou após um pequeno debounce e mostre um estado claro de “verificando”. Cancele ou ignore requisições antigas para que respostas lentas não sobrescrevam entradas mais recentes e criem erros confusos.

Qual a melhor forma de normalizar erros do servidor para a UI?

Normalize qualquer formato do backend em uma forma interna como { fieldErrors: { key: [messages] }, formErrors: [messages] }. Use um estilo de caminho consistente (notação ponto funciona bem) para que um campo address.street sempre possa ler fieldErrors['address.street'] sem casos especiais.

Como devo exibir erros e focar o campo certo após o submit?

Mostre erros de formulário no topo e erros de campo ao lado do input correspondente. Após um submit com falha, foque o primeiro campo com erro e limpe o erro de servidor daquele campo assim que o usuário começar a editá-lo novamente.

Fácil de começar
Criar algo espantoso

Experimente o AppMaster com plano gratuito.
Quando estiver pronto, você poderá escolher a assinatura adequada.

Comece