en
· 4 min de leitura

Formulários são sistemas distribuídos

Um formulário são duas máquinas, uma rede não confiável e um usuário que clica duas vezes. Desenhe como o sistema distribuído que ele é.

fullstackengineering

Um formulário parece a coisa mais simples do desenvolvimento web. Alguns inputs, um botão, um handler. Já encontrei mais bugs que perdem dinheiro em formulários do que em qualquer fila, cache ou banco que já auditei.

O motivo é que um formulário é um sistema distribuído disfarçado. São duas máquinas, o navegador e o servidor. Existe uma rede entre eles que perde pacotes, dá timeout e entrega atrasado. E existe um usuário que clica duas vezes, fecha a aba no meio da requisição e abre a página em duas janelas. Toda falha clássica de sistemas distribuídos está disponível, e a maioria dos formulários é escrita como se nenhuma delas existisse.

Envio duplicado é o padrão

A primeira falha é o duplo envio. O usuário clica, nada acontece visivelmente por 800 milissegundos, ele clica de novo. Duas requisições chegam. Dois pedidos são criados, ou dois pagamentos são cobrados, ou dois emails saem.

Desabilitar o botão no clique não é correção. É uma mitigação que falha no momento em que a rede reenvia um POST, o navegador reenvia depois de uma navegação para trás, ou uma segunda aba envia o mesmo rascunho. A correção mora no servidor: uma chave de idempotência gerada quando o formulário é renderizado, enviada com a submissão e checada antes de qualquer efeito colateral. A segunda requisição com a mesma chave retorna o resultado da primeira.

No Next.js com Server Actions, a chave viaja como campo oculto ou como argumento. A action busca a chave no banco na mesma transação que cria o registro. Se existe, retorna cedo. Essa única tabela com uma unique constraint já salvou mais receita do que qualquer quantidade de debounce no frontend.

A validação acontece duas vezes, e tudo bem

Validação no cliente é para o usuário. Validação no servidor é para o sistema. Não são redundantes, têm clientes diferentes, e pular a segunda porque a primeira existe é a brecha de segurança que mais encontro em auditorias.

O truque para manter as duas sincronizadas é um schema só, compartilhado. Defina o formato uma vez com uma biblioteca de validação em runtime, importe no Client Component para feedback instantâneo e na Server Action para a checagem de verdade. Quando um campo é adicionado, os dois lados ficam sabendo pelo mesmo arquivo.

O relato de erros precisa ser estruturado. Um formulário que retorna 'algo deu errado' jogou fora a informação que o usuário precisa para corrigir. Eu retorno um mapa de nome de campo para mensagem, e o hook de estado de formulário do framework coloca cada mensagem ao lado do seu input. O hook useActionState existe exatamente para isso: ele leva o resultado anterior de volta ao cliente sem biblioteca de estado.

Estado que sobrevive à rede

O usuário digitou por quatro minutos e o envio falhou porque a sessão expirou. Se o formulário reseta, você perdeu um cliente. Progressive enhancement é a base: um formulário que posta para uma Server Action funciona antes do JavaScript carregar e mantém os valores digitados na resposta do servidor.

Para formulários longos eu persisto rascunhos, seja em local storage ou no servidor como um registro explícito de rascunho, atrelado ao usuário. Autosave a cada poucos segundos, restauração ao carregar, e avisar o usuário que aconteceu. É uma feature pequena que transforma um abandono em uma visita de retorno.

Optimistic updates são a mesma ideia ao contrário. Mostre o resultado imediatamente, envie a requisição e reconcilie quando o servidor responder. O hook useOptimistic cuida da mecânica, mas a decisão de design é sua: o que o usuário vê se o servidor disser não? Decida isso antes de lançar.

Concorrência e a segunda aba

Duas janelas editam o mesmo registro. As duas enviam. A última escrita vence, e o trabalho do primeiro editor some em silêncio. É um lost update clássico, e a defesa é um número de versão. O formulário carrega a versão que ele leu. O update checa que a versão no banco ainda bate. Se não bate, rejeita com uma mensagem dizendo que outra pessoa mudou isso, e mostra a diferença.

Isso é optimistic concurrency control, e é uma coluna de inteiro. Adiciono em toda tabela que um humano edita.

Nada disso é exótico. É a mesma idempotência, validação, persistência e versionamento que qualquer sistema distribuído precisa. O formulário é só o lugar onde a maioria dos times esquece que está construindo um.