Envio de formulários HTML7 min de leitura
Validação de formulários HTML no navegador e no servidor
Valide um formulário com required, type, pattern e min, estilize erros com :user-invalid e aplique as mesmas regras no servidor com definições de campos.
Use as restrições nativas do HTML, como required, type="email", min, max e pattern, para que o navegador barre dados inválidos antes de qualquer envio. Depois aplique as mesmas regras no servidor, porque bots e scripts pulam o navegador por completo. Com o Ikiform, defina os campos do formulário e todo envio é conferido: um inválido recebe 422 validation_failed com uma mensagem por campo, que você pode mostrar com setCustomValidity.
O exemplo é um formulário de reserva de workshop. Ele tem o tipo de campo em que a validação faz diferença: um tamanho de equipe com limites, uma data que não pode estar no passado, um telefone num único formato e um código de reserva com formato fixo.
O que muda aqui
Existem dois validadores, e eles não veem as mesmas requisições. O navegador confere só as pessoas que usam a sua página, com ou sem scripts. O servidor confere tudo, inclusive bots enviando direto ao endpoint e o seu próprio código chamando form.submit(), que pula as checagens do navegador. O trabalho é fazer os dois dizerem a mesma coisa.
- Uma pessoa envia o formulário, e o navegador confere as restrições do HTML. Um campo inválido recebe uma mensagem e nada é enviado.
- Um envio válido chega ao Ikiform, que o confere com a definição de campos do formulário e armazena valores tipados.
- Um bot ou script envia direto ao endpoint, então nenhuma checagem do navegador roda.
- O Ikiform o confere com a mesma definição e o recusa com 422 antes de armazenar qualquer coisa.
A camada do navegador
Prévia
Roda o validador do Ikiform no seu navegador. Nada é enviado.
ws-12.<form id="workshop" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Your name</label>
<input id="name" name="name" autocomplete="name" maxlength="120" required />
<label for="email">Work email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="team_size">People attending</label>
<input id="team_size" name="team_size" type="number" min="2" max="20" step="1" required />
<label for="date">Preferred date</label>
<input id="date" name="date" type="date" min="2026-11-02" required />
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" placeholder="+14155550123"
pattern="\+[1-9][0-9]{6,14}" title="International format, like +14155550123" />
<label for="code">Booking code</label>
<input id="code" name="code" pattern="WS-[0-9]{4}" title="Codes look like WS-1234" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Request the workshop</button>
</form>Cada atributo corresponde a um estado de validade, que você pode ler em input.validity no console:
| Atributo | Estado de validade | Observações |
|---|---|---|
required | valueMissing | Num grupo de radio, um radio com required cobre o grupo |
type="email", type="url" | typeMismatch | Permissivo: a@b passa como e-mail nos navegadores |
pattern | patternMismatch | Precisa casar com o valor inteiro. Ignorado num campo opcional vazio |
minlength, maxlength | tooShort, tooLong | Só conferido depois que a pessoa edita o valor |
min, max | rangeUnderflow, rangeOverflow | Números, datas e horários |
step | stepMismatch | step="1" recusa decimais |
| Entrada que não pode ser interpretada | badInput | Letras digitadas num campo numérico em alguns navegadores |
setCustomValidity() | customError | Qualquer mensagem que você definir, até voltar a defini-la como vazia |
Patterns em dois motores
Os navegadores compilam pattern com a flag v, que é mais rígida que as expressões regulares que a maioria das pessoas escreve: um - literal dentro de uma classe de caracteres precisa ser escapado, como em [a-z\-]. O Ikiform compila o pattern de um campo com a flag u e também casa com o valor inteiro. Escreva patterns válidos nos dois, como o código de reserva acima, e teste cada um na prévia do formulário antes de publicar.
Estilizando erros
:invalid casa com um campo obrigatório vazio no momento em que a página carrega, então todo campo aparece em vermelho antes de alguém digitar. :user-invalid só casa depois que a pessoa interagiu com o campo ou tentou enviar, que é quase sempre o que você quer:
<style>
#workshop :user-invalid {
border-color: #b42318;
}
</style>A camada do servidor
Cole a definição acima na aba Fields (campos) do formulário. A partir daí, o Ikiform valida todo envio com base nela, venha de onde vier, e armazena valores convertidos: team_size como número, phone sem separadores, date como YYYY-MM-DD. Um envio que quebra uma regra é recusado, nada é armazenado, e a resposta diz qual campo falhou:
| Campo | Enviado | Mensagem |
|---|---|---|
team_size | 25 | Must be at most 20 |
team_size | 2.5 | Enter a whole number |
date | 2026-10-30 | Must be on or after 2026-11-02 |
phone | 415 555 0123 | Use international format, e.g. +14155550123 |
code | ws-12 | Codes look like WS-1234 |
website | Qualquer valor | Unknown field "website" |
A última linha importa. Com campos definidos, o Ikiform recusa qualquer campo que não está na definição, então um input renomeado falha de forma visível em vez de ser armazenado com o nome errado. Campos que começam com _ e o honeypot não contam. Todos os tipos e regras estão em Campos e validação.
Com um envio simples
Não precisa de código. Um envio simples recusado mostra a página de erro hospedada do Ikiform com o status 422, cada problema sob o rótulo do seu campo e um link para a página de onde o envio veio. O botão Back (voltar) do navegador geralmente mantém o que a pessoa digitou; o link recarrega o formulário vazio.
Mostrando erros do servidor nos campos
Com um script, passe cada mensagem do servidor para a interface de erro do próprio navegador. setCustomValidity marca o campo como inválido com esse texto, e reportValidity o mostra no balão de sempre e foca o primeiro:
const form = document.querySelector("#workshop");
for (const control of form.elements) {
control.addEventListener("input", () => control.setCustomValidity?.(""));
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const result = await response.json();
if (result.ok) {
form.replaceWith("Thanks. We'll confirm the date by email.");
return;
}
const fields = result.error.fields ?? {};
for (const [name, messages] of Object.entries(fields)) {
form.elements.namedItem(name)?.setCustomValidity?.(messages[0]);
}
if (!form.reportValidity()) return;
alert(result.error.message);
});- Limpe a mensagem personalizada no
input. Um campo com erro personalizado continua inválido até você voltá-lo para uma string vazia, e enquanto isso a pessoa não consegue enviar. - As checagens do navegador ainda rodam antes do evento
submit, então o servidor só vê envios que passaram por elas. - Se o erro não tem
fields, como429 rate_limited,reportValidity()devolvetruee o script recorre à mensagem geral.
Conferindo regras do servidor antes de enviar
Para pegar regras que só existem no servidor, como uma lista de domínios de e-mail, antes da requisição, carregue a definição pública do formulário e rode o validate do SDK, que aplica as mesmas regras e mensagens de campo no navegador:
import { getSchema, validate } from "@ikiform/sdk";
const form = document.querySelector("#workshop");
const result = await getSchema("frm_xxxxxxxxxxxxxxxx");
const fields = result.ok ? result.schema.fields : [];
form.addEventListener("submit", (event) => {
const errors = validate(fields, new FormData(form));
for (const control of form.elements) control.setCustomValidity?.(errors[control.name] ?? "");
if (!form.reportValidity()) event.preventDefault();
});getSchema só expõe definições de campos, que são públicas de qualquer forma: qualquer um pode lê-las em /f/<formId>/schema.Confira se funciona
- Em Allowed domains do formulário, adicione o domínio do seu site, mais
localhostenquanto desenvolve. Deixe vazio para aceitar envios de qualquer lugar. - Envie o formulário vazio. O navegador o barra em Seu nome e nada aparece no painel Network.
- Remova o
maxdo tamanho da equipe nas ferramentas de desenvolvedor, digite25e envie. O envio simples mostra a página de erro do Ikiform; o envio por script mostra Must be at most 20 no campo. - Envie com curl, pulando o navegador:
curl -H "Accept: application/json" -H "Origin: https://example.com" -d team_size=abc https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx, com o seu próprio domínio permitido como origem. A resposta é422com uma mensagem parateam_sizee para cada campo obrigatório que faltou. - Abra Submissions no painel. O novo envio está lá com todos os campos que você enviou.
Pegadinhas
| Problema | Causa e solução |
|---|---|
| O formulário nunca envia depois de um erro do servidor | Uma mensagem de validade personalizada nunca foi limpa. Zere-a com setCustomValidity("") no input |
minlength não dispara em valores pré-preenchidos | tooShort só vale depois que a pessoa edita o campo. A regra do servidor pega o caso |
Um pattern é ignorado por completo | Ele é inválido sob a flag v, e os navegadores ignoram um pattern inválido por completo. Escape - e outros caracteres especiais dentro de classes |
| Os erros aparecem antes de alguém digitar | O CSS usa :invalid. Troque por :user-invalid |
422 validation_failed | O envio não corresponde à definição de campos do formulário. error.fields aponta cada campo e o motivo. Faça o name do input bater com o id do campo. |
O fluxo completo de envio está na visão geral, e Erros lista todos os códigos que um envio pode receber.
Perguntas
Não. Ela só roda no navegador, na sua página, e bots ou scripts enviam direto ao endpoint. Valide no servidor também. Com o Ikiform, defina os campos do formulário e todo envio é conferido.
Chame input.setCustomValidity(message) e depois form.reportValidity(), que mostra a mensagem no balão de sempre do navegador. Limpe-a com setCustomValidity("") quando a pessoa editar o campo.
:invalid casa com qualquer campo que não atende às restrições, inclusive campos obrigatórios vazios no carregamento da página. :user-invalid só casa depois que a pessoa interagiu com o campo ou tentou enviar.
422 validation_failed, sem nada armazenado. Requisições JSON recebem error.fields, que mapeia cada id de campo para suas mensagens; envios simples de formulário recebem uma página de erro hospedada que as lista por rótulo do campo.
Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.