# 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.

Publicado em 2026-10-25 · https://ikiform.com/pt-br/guides/html-form-submission/validation

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.

Diagrama: Um envio passando pela validação do navegador e depois pela validação de campos do Ikiform

1. 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.
2. Um envio válido chega ao Ikiform, que o confere com a definição de campos do formulário e armazena valores tipados.
3. Um bot ou script envia direto ao endpoint, então nenhuma checagem do navegador roda.
4. O Ikiform o confere com a mesma definição e o recusa com 422 antes de armazenar qualquer coisa.

A camada do navegador é para as pessoas. A camada do servidor é a que realmente vale.

## A camada do navegador

Formulário: Book a workshop

- **Seu nome** (`name`, text, obrigatório)
- **E-mail de trabalho** (`email`, email, obrigatório)
- **Participantes** (`team_size`, number, obrigatório)
- **Data preferida** (`date`, date, obrigatório)
- **Telefone** (`phone`, phone)
- **Código de reserva** (`code`, text)

A definição que o Ikiform aplica. Teste um tamanho de equipe de 25, uma data antes de 2 de novembro ou um código como `ws-12`.

`workshop.html`:

```html
<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:

`workshop.html`:

```html
<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](https://ikiform.com/pt-br/docs/fields.md).

### 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:

`workshop.js`:

```js
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`, como `429 rate_limited`, `reportValidity()` devolve `true` e 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](https://ikiform.com/pt-br/docs/sdk.md), que aplica as mesmas regras e mensagens de campo no navegador:

`workshop.js`:

```js
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();
});
```

> Trate isso como uma conveniência. O servidor continua sendo a fonte da verdade, e `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

1. Em **Allowed domains** do formulário, adicione o domínio do seu site, mais `localhost` enquanto desenvolve. Deixe vazio para aceitar envios de qualquer lugar.
2. Envie o formulário vazio. O navegador o barra em **Seu nome** e nada aparece no painel Network.
3. Remova o `max` do tamanho da equipe nas ferramentas de desenvolvedor, digite `25` e envie. O envio simples mostra a página de erro do Ikiform; o envio por script mostra **Must be at most 20** no campo.
4. 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 é `422` com uma mensagem para `team_size` e para cada campo obrigatório que faltou.
5. 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](https://ikiform.com/pt-br/guides/html-form-submission.md#validation), e [Erros](https://ikiform.com/pt-br/docs/errors.md) lista todos os códigos que um envio pode receber.

## Perguntas

### A validação de formulários HTML basta sozinha?

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.

### Como mostrar uma mensagem de erro do servidor num input?

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.

### Qual a diferença entre :invalid e :user-invalid?

: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.

### O que o Ikiform devolve quando um campo falha na validação?

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.

## Mais guias

- [Envio de formulários HTML](https://ikiform.com/pt-br/guides/html-form-submission.md): O que o navegador faz quando um formulário é enviado: quais campos manda, como os codifica, o que o servidor deve responder e como o Ikiform responde.
- [Sem recarregar a página](https://ikiform.com/pt-br/guides/html-form-submission/without-page-reload.md): Envie um formulário com fetch e FormData para o visitante continuar na página, com erros de campo, estado de carregamento, novas tentativas e um plano B quando scripts falham.
- [O atributo action](https://ikiform.com/pt-br/guides/html-form-submission/action-attribute.md): O que o atributo action de um formulário faz, como valores relativos e vazios são resolvidos, como formaction manda um formulário a dois lugares e por que hosts estáticos respondem 405.
- [Redirecionar após o envio](https://ikiform.com/pt-br/guides/html-form-submission/redirect-after-submit.md): Mande visitantes para sua própria página de agradecimento depois do envio: o redirecionamento 303, o campo _redirect, quais domínios são permitidos e o que o refresh faz.
