Tudo que você precisa saber sobre CEP em JavaScript: validação, máscara, API e debounce
Quem já construiu um formulário de endereço no Brasil sabe que o campo de CEP parece simples, mas esconde algumas armadilhas. Neste artigo, vou cobrir do básico ao avançado: estrutura do CEP, validação com Regex, máscara de input, integração com a API ViaCEP, tratamento de erros e debounce — tudo em JavaScript puro, sem dependências externas.
1. O que é o CEP e como ele é estruturado?
O CEP (Código de Endereçamento Postal) é composto por 8 dígitos numéricos, divididos em dois grupos:
XXXXX - XXX
| |
Faixa Sufixo
- Os 5 primeiros dígitos identificam a localidade (região, cidade, bairro)
- Os 3 últimos dígitos identificam o logradouro ou setor específico
Exemplos reais:
| CEP | Local |
|---|---|
| 01001-000 | Praça da Sé, São Paulo - SP |
| 20040-020 | Centro, Rio de Janeiro - RJ |
| 30112-000 | Centro, Belo Horizonte - MG |
Detalhe importante: nem todo CEP aponta para uma rua específica. Grandes empresas e órgãos públicos possuem CEPs exclusivos (ex: Correios, Receita Federal).
2. Limpeza de Input (o primeiro passo que muitos pulam)
Antes de qualquer validação, precisamos normalizar o que o usuário digitou — removendo traços, espaços e qualquer caractere não numérico:
function limparCEP(cep) {
return cep.replace(/\D/g, "");
}
// Exemplos:
limparCEP("01001-000"); // "01001000"
limparCEP("01001 000"); // "01001000"
limparCEP(" 01001000 "); // "01001000"
A regex /\D/g substitui qualquer caractere que não seja dígito (\D) por string vazia, globalmente (g).
3. Aplicando Máscara ao Input
Para formatar o CEP enquanto o usuário digita (no formato XXXXX-XXX):
function formatarCEP(cep) {
const cepLimpo = limparCEP(cep);
// Aplica máscara apenas quando tiver 8 dígitos completos
if (cepLimpo.length === 8) {
return cepLimpo.replace(/^(\d{5})(\d{3})$/, "$1-$2");
}
return cepLimpo;
}
console.log(formatarCEP("01001000")); // "01001-000"
console.log(formatarCEP("0100100")); // "0100100" (incompleto, sem máscara)
Para aplicar a máscara em tempo real no input HTML:
const inputCEP = document.getElementById("cep");
inputCEP.addEventListener("input", (e) => {
const cepLimpo = limparCEP(e.target.value);
// Limita a 8 dígitos
const cepTruncado = cepLimpo.slice(0, 8);
// Aplica a máscara progressiva enquanto digita
if (cepTruncado.length > 5) {
e.target.value = cepTruncado.replace(/^(\d{5})(\d{0,3})$/, "$1-$2");
} else {
e.target.value = cepTruncado;
}
});
Por que máscara progressiva? Se você só aplicar a máscara quando tiver 8 dígitos, o usuário vê
01001000e só depois01001-000. Com a máscara progressiva, ele vê01001-assim que digita o 6º caractere — UX muito melhor.
4. Validação com Regex (antes de chamar qualquer API)
Nunca dispare uma requisição para a API sem validar antes. Isso economiza requisições desnecessárias e protege sua aplicação:
function validarCEP(cep) {
const cepLimpo = limparCEP(cep);
// Deve ter exatamente 8 dígitos numéricos
const regexCEP = /^[0-9]{8}$/;
return regexCEP.test(cepLimpo);
}
console.log(validarCEP("01001-000")); // true
console.log(validarCEP("00000-000")); // true (formalmente válido, mas pode não existir)
console.log(validarCEP("0100100")); // false (só 7 dígitos)
console.log(validarCEP("abcde-fgh")); // false
Atenção: A validação via Regex apenas verifica o formato, não se o CEP existe de fato nos Correios. Para isso, precisamos consultar a API.
5. Integrando com a API ViaCEP
O ViaCEP é um serviço gratuito, sem autenticação e muito confiável para consultas de CEP no Brasil.
async function consultarCEP(cep) {
const cepLimpo = limparCEP(cep);
// Validação antes da requisição
if (!validarCEP(cepLimpo)) {
throw new Error("Formato de CEP inválido.");
}
const response = await fetch(`https://viacep.com.br/ws/${cepLimpo}/json/`);
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
const data = await response.json();
// A API retorna { "erro": true } quando o CEP não existe
if (data.erro) {
throw new Error("CEP não encontrado.");
}
return data;
}
Estrutura do objeto retornado pela API:
{
"cep": "01001-000",
"logradouro": "Praça da Sé",
"complemento": "lado ímpar",
"bairro": "Sé",
"localidade": "São Paulo",
"uf": "SP",
"ibge": "3550308",
"ddd": "11",
"siafi": "7107"
}
Usando a função com tratamento de erros:
async function preencherEndereco(cep) {
try {
const endereco = await consultarCEP(cep);
document.getElementById("logradouro").value = endereco.logradouro;
document.getElementById("bairro").value = endereco.bairro;
document.getElementById("cidade").value = endereco.localidade;
document.getElementById("uf").value = endereco.uf;
} catch (error) {
console.error("Falha ao buscar CEP:", error.message);
// Aqui você pode mostrar uma mensagem de erro na UI
}
}
6. Debounce: a técnica que evita spam de requisições
Um erro clássico é disparar a busca a cada tecla digitada. Com debounce, você espera o usuário parar de digitar antes de fazer a requisição:
function debounce(fn, delay = 500) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Cria uma versão "debounced" da função de busca
const buscarCEPComDebounce = debounce(async (cep) => {
if (validarCEP(cep)) {
await preencherEndereco(cep);
}
}, 600);
// Conecta ao input
document.getElementById("cep").addEventListener("input", (e) => {
buscarCEPComDebounce(e.target.value);
});
onBlur vs debounce — quando usar cada um?
| Situação | Recomendação |
|---|---|
| Formulário simples, usuário já sabe o CEP | onBlur (mais simples, menos requisições) |
| UX fluida, preenchimento automático rápido | debounce no onChange (300–600ms) |
| Mobile com teclado numérico | onBlur (evita busca durante digitação lenta) |
7. Feedback visual durante o carregamento
Um detalhe que melhora muito a UX: mostrar ao usuário que a busca está acontecendo:
async function buscarComFeedback(cep) {
const input = document.getElementById("cep");
const status = document.getElementById("cep-status");
if (!validarCEP(cep)) return;
// Estado de carregamento
input.disabled = true;
status.textContent = "Buscando...";
try {
const endereco = await consultarCEP(cep);
preencherCampos(endereco);
status.textContent = "✓ Endereço encontrado";
status.style.color = "green";
} catch (error) {
status.textContent = "✗ " + error.message;
status.style.color = "red";
} finally {
// Sempre reabilita o input, mesmo em caso de erro
input.disabled = false;
}
}
8. Tratamento de edge cases que passam despercebidos
// CEPs que formalmente são válidos mas costumam não existir:
const CEPS_INVALIDOS_CONHECIDOS = [
"00000000", // Zerado
"11111111", // Sequência repetida
"99999999", // Máximo
];
function cepSuspeito(cep) {
const limpo = limparCEP(cep);
// Sequências repetidas (00000000, 11111111, etc.)
if (/^(\d)\1{7}$/.test(limpo)) return true;
return CEPS_INVALIDOS_CONHECIDOS.includes(limpo);
}
console.log(cepSuspeito("00000000")); // true
console.log(cepSuspeito("12345678")); // false
console.log(cepSuspeito("01001000")); // false
Solução Completa — Juntando tudo
// utils/cep.js
const limparCEP = (cep) => cep.replace(/\D/g, "");
const formatarCEP = (cep) => {
const limpo = limparCEP(cep).slice(0, 8);
return limpo.length > 5
? limpo.replace(/^(\d{5})(\d{0,3})$/, "$1-$2")
: limpo;
};
const validarCEP = (cep) => /^[0-9]{8}$/.test(limparCEP(cep));
const consultarCEP = async (cep) => {
const limpo = limparCEP(cep);
if (!validarCEP(limpo)) throw new Error("CEP inválido");
const res = await fetch(`https://viacep.com.br/ws/${limpo}/json/`);
if (!res.ok) throw new Error(`Erro ${res.status}`);
const data = await res.json();
if (data.erro) throw new Error("CEP não encontrado");
return data;
};
const debounce = (fn, ms = 500) => {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
};
export { limparCEP, formatarCEP, validarCEP, consultarCEP, debounce };
Conclusão
Um campo de CEP bem implementado combina:
- Limpeza — normaliza o input antes de qualquer processamento
- Máscara progressiva — melhora a UX durante a digitação
- Validação local — evita requisições desnecessárias
- Debounce inteligente — escolha baseada no contexto (onBlur vs onChange)
- Tratamento de erros robusto — distingue erro de rede de CEP inexistente
- Feedback visual — mantém o usuário informado durante a busca
Para testar rapidamente um CEP durante o desenvolvimento sem montar um formulário inteiro, ferramentas como o cep-br.com permitem validar e consultar o retorno da API de forma direta — útil para debugar edge cases antes de integrar no código.
Pergunta para a comunidade: Vocês costumam usar onBlur ou debounce no onChange nos formulários de vocês? E quando o CEP não é encontrado, vocês limpam os campos de endereço ou mantêm o que estava preenchido antes? Qual abordagem tem funcionado melhor na UX de vocês?
Fonte: https://cep-br.com/