Voltar ao blog
Tutorial

Formulários que convertem: validação, UX e integração

Por Flávio Emanuel · · 7 min de leitura

Passei a tarde inteira analisando formulários de clínicas odontológicas. Sabe aqueles formulários que pedem 15 campos antes de deixar você enviar uma mensagem? Pois é. Perdi a conta de quantas pessoas desistem no meio.

A verdade é simples: formulários ruins custam clientes. Eu vejo isso toda semana nos meus projetos.

Um formulário longo espanta seu lead

Quando você coloca muitos campos num formulário, está pedindo demais antes de oferecer algo. A pessoa só quer marcar uma consulta, não responder a um questionário. Você precisa saber quantos campos são realmente necessários para iniciar a conversa.

Em uma clínica? Três campos bastam: nome, telefone e melhor horário. Tudo mais pode esperar. Quando o lead retornar, você pede mais informações. Isso é conversa. Formulário é abertura.

Validação client e server-side não são a mesma coisa

A validação no navegador é rápida e dá feedback imediato. O usuário digita um email errado e já vê a mensagem vermelha. Mas nunca confie só nisso. Qualquer pessoa consegue burlar JavaScript.

No servidor você valida de novo. Sempre. Você recebe os dados, limpa, valida cada campo. Em Supabase, uso uma função edge que checa tipo, tamanho, formato. Se não passar, retorna erro com código HTTP 400. Simples.

Meu padrão é: client-side para UX, server-side para segurança.

Loading state é a diferença entre profissional e amador

Quando o formulário é enviado, o botão precisa mudar. Texto muda de “Enviar” para “Aguarde…”. O botão fica desativado. Sem isso, o usuário clica de novo. E de novo. Você recebe a mesma mensagem 3 vezes.

Em Astro/React, uso um estado simples:

const [isLoading, setIsLoading] = useState(false);

const handleSubmit = async (e) => {
  e.preventDefault();
  setIsLoading(true);

  try {
    const response = await fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify(formData)
    });

    if (response.ok) {
      // sucesso
    }
  } finally {
    setIsLoading(false);
  }
};

Parece trivial. Mas funciona.

Mensagens de erro claras não são negociáveis

“Erro ao processar formulário” não ajuda ninguém. Onde foi o erro? Email? Telefone? Servidor caiu?

Eu digo exatamente o que aconteceu:

  • “Email inválido”
  • “Telefone precisa de 10 dígitos”
  • “Esse horário não está disponível”

O usuário entende e corrige. Sem frustração.

Integração com Resend, Supabase e WhatsApp

Depois que o formulário é validado, preciso fazer algo com os dados. Meu fluxo padrão:

  1. Salvo no Supabase: nome, email, telefone, data/hora
  2. Envio email via Resend para a clínica (R$ 0,10 por email depois dos 100 primeiros grátis)
  3. Jogo um link pro WhatsApp da clínica (sem API necessária, só wa.me/55...)

Isso acontece em uma rota API. Em Astro, crio um arquivo em src/pages/api/contact.ts:

export async function POST({ request }) {
  const data = await request.json();

  // validação
  if (!data.name || data.name.length < 2) {
    return new Response('Nome inválido', { status: 400 });
  }

  // salva no Supabase
  await supabase.from('leads').insert([data]);

  // envia email via Resend
  await resend.emails.send({
    from: 'noreply@clinica.com.br',
    to: data.email,
    subject: 'Sua consulta foi solicitada',
    html: `Oi ${data.name}...`
  });

  return new Response(JSON.stringify({ success: true }));
}

Simples e funciona todo dia.

Formulário curto é formulário que converte

Eu construí um para a Autopars Pro que tinha 4 campos. Conversão? 32%. Um cliente antes tinha 12 campos. Conversão? 8%.

A diferença é absurda porque estamos respeitando o tempo da pessoa.

Feedback visual conta

Quando o email é enviado com sucesso, mostro uma mensagem verde no lugar do formulário. “Mensagem recebida! Você vai receber um WhatsApp em breve.” Isso tranquiliza.

Se der erro, mostro em vermelho o que aconteceu. Nunca deixo o usuário no escuro.

Checklist para formulários que funcionam

  • Máximo 5 campos no primeiro contato
  • Validação no navegador com feedback imediato
  • Validação no servidor antes de salvar qualquer coisa
  • Loading state no botão durante envio
  • Mensagens de erro específicas, não genéricas
  • Integração com email (Resend) e WhatsApp
  • Teste em mobile (a maioria vira de celular)
  • Supabase ou banco de dados para histórico

Formulários chatos que pedem tudo de primeira morrem abandonados. Você quer conversa, não interrogatório.

Honeypot contra bots

Um detalhe que protege contra spam: honeypot.

Você coloca um campo escondido no formulário via CSS (display: none):

<input type="text" name="website" style="display: none;">

Bots vão tentar preencher. Usuários reais não veem.

No servidor:

if (data.website) {
  // é bot, rejeita
  return new Response('Invalid', { status: 400 });
}

Simples, efetivo, sem usar reCAPTCHA (que incomoda usuários).

Redirect inteligente vs popup

Depois que formulário envia:

Opção 1: popup “Sucesso! Mensagem enviada”. Opção 2: redirect pra página de obrigado.

Qual funciona melhor? Redirect.

Por quê? Porque a maioria dos leads quer finalizar a conversa depois. Não quer ler mensagem de popup. Quer sair da página.

Redirect pra página que diz:

“Sua consulta foi recebida! Você vai receber uma ligação em 24h. Enquanto isso, conheça nossos serviços.”

(e coloca links internos)

User sai feliz, vira visita interna, sai com página visitada. Melhor.

Testabilidade: qual ferramenta testar

Resend é bom pra emailer, mas como testa em dev?

Dois jeitos:

  1. Modo sandbox: Resend fornece email sandbox. Testa sem enviar de verdade.
  2. Mailtrap: ferramenta gratuita que captura emails em dev. Mostra exatamente o que foi enviado.

Eu uso Mailtrap em dev, Resend em prod.

Erro 422 vs 400: quando qual

HTTP 400 é “request ruim do lado do cliente” (email inválido, campo obrigatório faltando).

HTTP 422 é “request válido sintaticamente mas semanticamente impossível” (email existe, usuário já tem agendamento nesse horário).

Diferença não é óbvia. Mas é importante pra logging.

Meu padrão:

  • 400: erro de validação
  • 422: conflito de lógica
  • 500: erro de servidor (banco de dados, Resend down, etc)

Aí no client você mostra mensagem diferente:

  • 400: “Erro no formulário. Verifique os campos.”
  • 422: “Horário indisponível. Escolha outro.”
  • 500: “Erro temporário. Tente de novo.”

Performance: preload de recurso

Um detalhe pequeno que ajuda:

<link rel="preload" as="script" href="/api/contact" />

Browser já começa a reservar a conexão pro endpoint antes do user clicar.

Ganha 50ms de latência.

Não é muito, mas soma.

Analytics: rastreie drops

Qual é o maior drop do seu formulário? Campo 1? Campo 3? Botão submit?

Você consegue rastrear via analytics simples:

field.addEventListener('blur', () => {
  analytics.track('form_field_blur', { field: 'email' });
});

Se um campo tem 40% de blur mas 20% de preenchimento, é problema ali.

Se 80% clica no submit mas 60% completa, talvez timeout ou erro.

Dados desses eventos ajudam a debugar onde o formulário morre.

  • Implementar validação dupla (client+server)
  • Adicionar honeypot contra bots
  • Setup Resend e Mailtrap
  • Teste carregamento (loading state correto?)
  • Rastreie drops com analytics
  • Testa em mobile (a maioria vem de celular)
  • Implementa redirect pra página obrigado
  • Valida mensagens de erro específicas

Leia também: Anatomia de uma LP que converte | Supabase + React | Integrações com APIs e webhooks

Formulários chatos que pedem tudo de primeira morrem abandonados. Você quer conversa, não interrogatório.

Próximo passo

Precisa de um dev que entrega de verdade?

Seja pra um projeto pontual, reforço no time, ou parceria de longo prazo. Vamos conversar.

Falar no WhatsApp

Respondo em até 2h durante horário comercial.