Campo que só aceita números: defina o número e confira no servidor
Quantidade, código com zeros, CEP e preço são "números" diferentes. Escolha o input certo, escreva a regra de cada um e valide no servidor sem cair nas armadilhas do $ e do \d.
Na tela de entrada de estoque de uma loja de autopeças, o campo de quantidade bloqueia letras no teclado. Mesmo assim, o banco tem um registro com quantidade "12a" convertida em 12, e um código de peça "00012345" que virou 12345 e não casa mais com o fornecedor. A resposta curta: "só números" não é uma regra, são várias. Defina o que é número em cada campo (inteiro com faixa, código de tamanho fixo que continua texto, decimal com vírgula), use no navegador um input que ajude a digitar e valide de novo no servidor, recusando o valor com uma mensagem em vez de "limpar" o que veio. E, se usar expressão regular, cuidado com o $ e o \d, que não significam a mesma coisa em todas as linguagens.
O bloqueio de teclado e a máscara são tratados no guia a máscara não é a validação. Aqui o foco é a regra: o que cada campo aceita e como conferir isso do jeito certo.
Por que "só números" engana
O teclado não é a única porta de entrada. A pessoa cola um texto, o gerenciador de formulários preenche sozinho, um script integra com a API, alguém monta a requisição na mão. Tudo o que o navegador faz é conforto; o servidor recebe texto e precisa decidir.
O segundo engano é tratar todos os campos numéricos como iguais. Na loja, são quatro casos:
- Quantidade: inteiro de 1 a 9999. É um número de verdade, que se soma.
- Código da peça: exatamente 8 algarismos, com zeros à esquerda. Parece número, mas é um identificador. Guardado como inteiro, perde os zeros.
- CEP: 8 algarismos. A pessoa digita "01310-100"; o hífen é um separador conhecido e pode ser removido de propósito.
- Preço: decimal com vírgula e duas casas. Uma regra de "só algarismos" recusaria um preço válido.
Como definir e validar cada campo
- Escreva a regra de cada campo numa frase. "Quantidade: inteiro de 1 a 9999, só algarismos." Essa frase vira a mensagem de erro.
- Escolha o tipo de armazenamento. Quantidade é inteiro; código e CEP são texto; preço vira centavos em inteiro, como em dinheiro em centavos.
- No navegador, use input de texto com teclado numérico para códigos e quantidades.
- Normalize só o que é separador conhecido, como o hífen do CEP. Nunca remova letras em silêncio.
- Valide no servidor com a mesma regra e responda com o nome do campo e a mensagem.
- Teste a API diretamente, sem passar pela tela.
O input certo no navegador
O type="number" parece a escolha óbvia, mas foi pensado para quantidades que se ajustam com setas. Ele aceita caracteres como e, + e - (porque 1e3 é um número válido), em alguns navegadores muda o valor quando a pessoa rola a página com o campo em foco, e o seu valueAsNumber transforma "00012345" em 12345. Para código, CEP e quantidade simples, o arranjo mais previsível é texto com teclado numérico:
<label for="codigo">Código da peça</label>
<input id="codigo" name="codigo" type="text" inputmode="numeric"
autocomplete="off" maxlength="8" aria-describedby="codigo-ajuda">
<p id="codigo-ajuda">8 algarismos, como aparece na nota do fornecedor. Ex.: 00012345</p>
<label for="quantidade">Quantidade</label>
<input id="quantidade" name="quantidade" type="text" inputmode="numeric"
autocomplete="off" aria-describedby="quantidade-ajuda">
<p id="quantidade-ajuda">De 1 a 9999.</p>
O inputmode="numeric" abre o teclado de algarismos no celular, sem impor as regras do type="number". A ajuda com exemplo fica visível, ligada ao campo por aria-describedby.
As regras em JavaScript
As mesmas funções podem rodar no navegador, para dar retorno rápido, e no servidor Node, onde a decisão vale:
function lerQuantidade(bruto) {
const texto = String(bruto ?? '').trim()
if (!/^[0-9]{1,4}$/.test(texto)) {
return { erro: 'Quantidade: use só algarismos, de 1 a 9999.' }
}
const valor = Number(texto)
if (valor < 1) return { erro: 'Quantidade: o mínimo é 1.' }
return { valor }
}
function lerCodigoPeca(bruto) {
const texto = String(bruto ?? '').trim()
if (!/^[0-9]{8}$/.test(texto)) {
return { erro: 'Código da peça: são exatamente 8 algarismos. Ex.: 00012345.' }
}
return { valor: texto }
}
function lerCep(bruto) {
const texto = String(bruto ?? '').trim().replace(/^([0-9]{5})-([0-9]{3})$/, '$1$2')
if (!/^[0-9]{8}$/.test(texto)) {
return { erro: 'CEP: informe 8 algarismos, com ou sem hífen. Ex.: 01310-100.' }
}
return { valor: texto }
}
function lerPrecoEmCentavos(bruto) {
const m = /^([0-9]{1,7}),([0-9]{2})$/.exec(String(bruto ?? '').trim())
if (!m) return { erro: 'Preço: use vírgula e duas casas. Ex.: 189,90.' }
return { valor: Number(m[1]) * 100 + Number(m[2]) }
}
Repare que o código da peça volta como texto, com os zeros. O CEP só tem o hífen removido quando ele está exatamente na posição esperada; "0131a-100" continua sendo recusado. E o preço aceita só o formato que a tela pede. Se você quiser aceitar "1.234,50", acrescente isso à regra de propósito, com teste, em vez de apagar pontos no escuro.
As armadilhas do $ e do \d
A expressão ^\d+$ é a resposta mais copiada para "só números", e ela se comporta diferente conforme a linguagem.
Em JavaScript, sem a flag m, o $ casa só no fim do texto e o \d equivale a [0-9]. Lá, ela faz o que promete. Em PHP (PCRE) e em .NET, o $ também casa antes de uma quebra de linha final. Ou seja, "123\n" passa. Em .NET, além disso, o \d aceita algarismos de outros sistemas de escrita, como os arábico-índicos:
Regex.IsMatch("123\n", @"^\d+$"); // true: o $ aceitou a quebra de linha
Regex.IsMatch("١٢٣", @"^\d+$"); // true: \d aceita algarismos Unicode
Regex.IsMatch("123\n", @"^[0-9]+\z"); // false: [0-9] e \z fecham as duas portas
A correção é usar [0-9] no lugar de \d e \z no lugar de $ para o fim absoluto do texto. Em PHP, \z também funciona, assim como o modificador D. No C#, para inteiros, você pode dispensar a regex e confiar no parser com estilo estrito:
using System.Globalization;
public static bool TentarLerQuantidade(string? bruto, out int quantidade)
{
var texto = (bruto ?? "").Trim();
return int.TryParse(texto, NumberStyles.None, CultureInfo.InvariantCulture, out quantidade)
&& quantidade is >= 1 and <= 9999;
}
Com NumberStyles.None, o TryParse recusa sinal, espaço interno, separador de milhar e ponto decimal. O Trim antes dele é uma decisão explícita: espaço nas pontas é tolerado; qualquer outra coisa, não.
A resposta do servidor
Quando a regra falha, o servidor recusa a requisição inteira, com status 422 e a mensagem por campo, a mesma frase que a tela usa (o guia mensagem de erro igual na API mostra como manter uma fonte só). O teste que importa é o que passa por fora da tela:
curl -i -X POST https://loja.exemplo.com.br/api/estoque/entradas \
-H "Content-Type: application/json" \
-d '{"codigo":"00012345","quantidade":"12a"}'
A resposta esperada:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{"erros":{"quantidade":"Quantidade: use só algarismos, de 1 a 9999."}}
Se a API devolver 200 e gravar 12, ela está decidindo pela pessoa. "12a" pode ser um 12 com a tecla vizinha apertada, ou um 124 em que o 4 virou "a". Só quem digitou sabe.
Erros comuns
- Validar só no navegador. Qualquer chamada direta à API grava o que quiser.
- Remover letras em silêncio e gravar o resto. "12a" vira 12 e ninguém fica sabendo que o valor mudou.
- Guardar código com zeros à esquerda como inteiro. "00012345" vira 12345 e para de casar com o sistema do fornecedor.
- Usar
^\d+$em .NET ou PHP. Quebra de linha final e algarismos de outros alfabetos passam pela validação. - Usar a regra de inteiro para preço. A vírgula é recusada ou removida, e 189,90 vira 18990 reais.
- Usar
type="number"para código. A rolagem do mouse altera o valor e o zero à esquerda some.
Checklist
- Cada campo numérico tem uma frase que diz o que ele aceita.
- Códigos, CEP e documentos são guardados como texto.
- Valores monetários são convertidos para centavos com regra explícita.
- Os inputs de código e quantidade usam texto com
inputmode="numeric". - O servidor valida com a mesma regra e responde 422 com o campo.
- Expressões regulares usam
[0-9]e fim absoluto quando a linguagem exige. - Existe um teste que envia "12a" direto à API e espera recusa.
Envie "12a" e "123" seguido de quebra de linha direto para a API do seu formulário mais usado. Ela recusa os dois, ou algum deles foi gravado?
Referência: https://stackoverflow.com/questions/19715303/regex-that-accepts-only-numbers-0-9-and-no-characters