0

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:

CEPLocal
01001-000Praça da Sé, São Paulo - SP
20040-020Centro, Rio de Janeiro - RJ
30112-000Centro, 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ê 01001000 e só depois 01001-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çãoRecomendação
Formulário simples, usuário já sabe o CEPonBlur (mais simples, menos requisições)
UX fluida, preenchimento automático rápidodebounce no onChange (300–600ms)
Mobile com teclado numéricoonBlur (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:

  1. Limpeza — normaliza o input antes de qualquer processamento
  2. Máscara progressiva — melhora a UX durante a digitação
  3. Validação local — evita requisições desnecessárias
  4. Debounce inteligente — escolha baseada no contexto (onBlur vs onChange)
  5. Tratamento de erros robusto — distingue erro de rede de CEP inexistente
  6. 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?

Carregando publicação patrocinada...