Artigo Build·Desenvolvimento·13 min de leitura de leitura

Validação de Documentos Brasileiros em Formulários (2026): CPF, CNPJ, CEP, Telefone

Formulário web brasileiro tem peculiaridades que frustram usuários e desenvolvedores: CPF aceita dígito verificador inválido, CNPJ muda formato, telefone tem celular com 9 dígito, CEP preenche endereço. Este guia cobre validação e máscara de todos os documentos comuns, com código pronto.

Vitor Morais

Por Vitor Morais

Fundador do MochaLabz ·

Valide CPF/CNPJ na hora

Cole o número, confirme módulo 11 e veja formatação correta.

Usar validador →

Formulário web brasileiro tem uma coleção de peculiaridades que frustra usuários e desenvolvedores: CPF que passa em regex mas falha no módulo 11, CNPJ que muda formato dependendo do layout, telefone celular com ou sem 9 inicial, CEP que preenche endereço via API, PIS que poucos sabem validar. Cada um desses documentos tem suas regras — tratar todos igualmente é garantia de bug.

Este guia cobre os documentos mais comuns (CPF, CNPJ, CEP, telefone, PIS, RG), validação client-side com UX correta, regex e algoritmos de módulo 11, máscara em tempo real, integração com API de CEP e estratégias de backend para revalidar com segurança.

Princípios gerais antes de cada documento

Frontend valida para UX; backend valida para segurança

Todo formulário sério tem validação dupla. Frontend evita que usuário envie dados óbvios incorretos (feedback em tempo real, reduz frustração). Backend é a verdade — usuário malicioso pode passar frontend facilmente.

Normalize antes de validar

Remova tudo que não é dígito antes de validar CPF, CNPJ, CEP, telefone. O usuário pode colar com ou sem formatação — sua lógica deve aceitar os dois.

Armazene limpo, exiba formatado

Banco guarda 11 dígitos do CPF (string “12345678909”). Interface mostra “123.456.789-09” aplicando máscara no render. Economiza espaço, evita inconsistência e facilita queries.

CPF: módulo 11 + regra de dígitos iguais

Regex de formato

const cpfRegex = /^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$/; cpfRegex.test("123.456.789-09"); // true cpfRegex.test("12345678909"); // true (sem formatação) cpfRegex.test("123.456.789.09"); // false (formato errado) cpfRegex.test("000.000.000-00"); // true (formato OK mas inválido!)

Atenção

Regex sozinha aceita 000.000.000-00, 111.111.111-11 e outras sequências repetidas. Esses CPFs passam no módulo 11 mas não existem na Receita Federal. Descartar sequências repetidas é obrigatório — ver função completa abaixo.

Validação completa com módulo 11

export function validarCPF(cpf: string): boolean { // Normaliza: remove tudo que não é dígito const digitos = cpf.replace(/\D/g, ""); // CPF tem 11 dígitos if (digitos.length !== 11) return false; // Rejeita sequências repetidas (000...00, 111...11, etc.) if (/^(\d)\1{10}$/.test(digitos)) return false; // Calcula primeiro dígito verificador let sum = 0; for (let i = 0; i < 9; i++) { sum += parseInt(digitos[i]) * (10 - i); } let d1 = 11 - (sum % 11); if (d1 >= 10) d1 = 0; if (d1 !== parseInt(digitos[9])) return false; // Calcula segundo dígito verificador sum = 0; for (let i = 0; i < 10; i++) { sum += parseInt(digitos[i]) * (11 - i); } let d2 = 11 - (sum % 11); if (d2 >= 10) d2 = 0; return d2 === parseInt(digitos[10]); } // Uso validarCPF("123.456.789-09"); // true (exemplo válido) validarCPF("000.000.000-00"); // false (repetição) validarCPF("123.456.789-00"); // false (dígito errado)

Máscara com IMask (React)

import { IMaskInput } from "react-imask"; import { useState } from "react"; export function InputCPF() { const [value, setValue] = useState(""); const [error, setError] = useState<string | null>(null); const handleBlur = () => { const digitos = value.replace(/\D/g, ""); if (digitos.length === 0) { setError(null); return; } if (digitos.length !== 11 || !validarCPF(value)) { setError("CPF inválido"); } else { setError(null); } }; return ( <div> <label htmlFor="cpf">CPF</label> <IMaskInput id="cpf" mask="000.000.000-00" value={value} onAccept={(val: string) => setValue(val)} onBlur={handleBlur} placeholder="000.000.000-00" aria-invalid={!!error} aria-describedby={error ? "cpf-error" : undefined} /> {error && ( <span id="cpf-error" className="text-red-600 text-sm"> {error} </span> )} </div> ); }

CNPJ: 14 dígitos e pesos diferentes

Regex de formato

const cnpjRegex = /^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$/; cnpjRegex.test("12.345.678/0001-90"); // true cnpjRegex.test("12345678000190"); // true (sem formatação)

Validação completa

export function validarCNPJ(cnpj: string): boolean { const digitos = cnpj.replace(/\D/g, ""); if (digitos.length !== 14) return false; if (/^(\d)\1{13}$/.test(digitos)) return false; // Pesos do primeiro dígito verificador const pesos1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; let sum = 0; for (let i = 0; i < 12; i++) { sum += parseInt(digitos[i]) * pesos1[i]; } let d1 = 11 - (sum % 11); if (d1 >= 10) d1 = 0; if (d1 !== parseInt(digitos[12])) return false; // Pesos do segundo dígito verificador const pesos2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; sum = 0; for (let i = 0; i < 13; i++) { sum += parseInt(digitos[i]) * pesos2[i]; } let d2 = 11 - (sum % 11); if (d2 >= 10) d2 = 0; return d2 === parseInt(digitos[13]); }

Máscara CNPJ

<IMaskInput mask="00.000.000/0000-00" value={value} onAccept={setValue} placeholder="00.000.000/0000-00" />

CEP: validação simples + busca automática

Regex

const cepRegex = /^\d{5}-?\d{3}$/; cepRegex.test("01001-000"); // true cepRegex.test("01001000"); // true cepRegex.test("01001-00"); // false

Busca de endereço via ViaCEP

interface EnderecoViaCEP { logradouro: string; bairro: string; localidade: string; // cidade uf: string; erro?: boolean; } export async function buscarCEP(cep: string): Promise<EnderecoViaCEP | null> { const digitos = cep.replace(/\D/g, ""); if (digitos.length !== 8) return null; try { const res = await fetch(`https://viacep.com.br/ws/${digitos}/json/`); if (!res.ok) return null; const data = await res.json(); if (data.erro) return null; return data; } catch { return null; } }

Formulário com preenchimento automático

"use client"; import { useState } from "react"; export function FormularioEndereco() { const [cep, setCep] = useState(""); const [logradouro, setLogradouro] = useState(""); const [bairro, setBairro] = useState(""); const [cidade, setCidade] = useState(""); const [uf, setUf] = useState(""); const [loading, setLoading] = useState(false); const handleCepBlur = async () => { setLoading(true); const endereco = await buscarCEP(cep); if (endereco) { setLogradouro(endereco.logradouro); setBairro(endereco.bairro); setCidade(endereco.localidade); setUf(endereco.uf); } setLoading(false); }; return ( <form> <IMaskInput mask="00000-000" value={cep} onAccept={setCep} onBlur={handleCepBlur} placeholder="00000-000" /> {loading && <span>Buscando...</span>} <input value={logradouro} onChange={(e) => setLogradouro(e.target.value)} placeholder="Rua" /> <input value={bairro} onChange={(e) => setBairro(e.target.value)} placeholder="Bairro" /> <input value={cidade} onChange={(e) => setCidade(e.target.value)} placeholder="Cidade" /> <input value={uf} onChange={(e) => setUf(e.target.value)} placeholder="UF" maxLength={2} /> <input placeholder="Número" /> <input placeholder="Complemento (opcional)" /> </form> ); }

Dica

Em 2026, além do ViaCEP, existem alternativas como BrasilAPI (brasilapi.com.br/api/cep/v2/{cep}), AwesomeAPI (cep.awesomeapi.com.br/json/{cep}) e Widenet. Se uma API cai, sua aplicação também cai — considere fallback usando 2-3 providers sequencialmente.

Telefone brasileiro: celular vs fixo

Estrutura

  • Celular (após 2018): 11 dígitos — (DD) + 9 + 8 dígitos. Ex: (11) 98765-4321.
  • Fixo: 10 dígitos — (DD) + 4 dígitos iniciais + 4 finais. Ex: (11) 3456-7890.

Regex flexível para ambos

const telefoneRegex = /^\(?\d{2}\)?\s?9?\d{4,5}-?\d{4}$/; // Casos aceitos telefoneRegex.test("(11) 98765-4321"); // celular telefoneRegex.test("(11) 3456-7890"); // fixo telefoneRegex.test("11987654321"); // sem formatação telefoneRegex.test("1134567890"); // fixo sem formatação

Validação específica

export function validarTelefoneBR(telefone: string): { valido: boolean; tipo?: "celular" | "fixo"; normalizado?: string; } { const digitos = telefone.replace(/\D/g, ""); // Remove código país se presente const semCodigoPais = digitos.startsWith("55") && digitos.length > 11 ? digitos.substring(2) : digitos; if (semCodigoPais.length === 11) { // Celular (DDD + 9 + 8 dígitos) const nonoDigito = semCodigoPais.charAt(2); if (nonoDigito !== "9") return { valido: false }; return { valido: true, tipo: "celular", normalizado: semCodigoPais }; } if (semCodigoPais.length === 10) { // Fixo (DDD + 8 dígitos) return { valido: true, tipo: "fixo", normalizado: semCodigoPais }; } return { valido: false }; } // Uso validarTelefoneBR("(11) 98765-4321"); // { valido: true, tipo: "celular" } validarTelefoneBR("(11) 3456-7890"); // { valido: true, tipo: "fixo" } validarTelefoneBR("123"); // { valido: false }

Máscara dinâmica (muda de 10 para 11 dígitos)

import { IMaskInput } from "react-imask"; <IMaskInput mask={[ { mask: "(00) 0000-0000" }, // fixo { mask: "(00) 00000-0000" }, // celular ]} value={value} onAccept={setValue} placeholder="(00) 00000-0000" />

PIS/PASEP: 11 dígitos com módulo 11

export function validarPIS(pis: string): boolean { const digitos = pis.replace(/\D/g, ""); if (digitos.length !== 11) return false; if (/^(\d)\1{10}$/.test(digitos)) return false; const pesos = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; let sum = 0; for (let i = 0; i < 10; i++) { sum += parseInt(digitos[i]) * pesos[i]; } let d = 11 - (sum % 11); if (d >= 10) d = 0; return d === parseInt(digitos[10]); }

RG: o documento que NÃO tem validação universal

RG é emitido por cada estado com formato diferente. Não há algoritmo universal de validação. Estratégia comum:

  • Regex básico (aceita letras e números): /^[A-Z0-9.-]{5,}$/i
  • Não valide dígito verificador (cada estado usa lógica diferente).
  • Confie em validação backend pelo próprio sistema do cliente.

Contexto

Para aplicações críticas que dependem de RG (banco, cartório), integre com API Serasa ou SPC que consulta bases reais. Para formulários comuns, só validação de formato e tamanho mínimo é suficiente.

Biblioteca completa: brazilian-values

npm install brazilian-values
import { isCPF, isCNPJ, isCEP, isPIS, formatToCPF, formatToCNPJ, formatToPhone, } from "brazilian-values"; // Validação isCPF("123.456.789-09"); // true/false isCNPJ("12.345.678/0001-90"); // true/false isCEP("01001-000"); // true/false isPIS("12345678901"); // true/false // Formatação formatToCPF("12345678909"); // "123.456.789-09" formatToCNPJ("12345678000190"); // "12.345.678/0001-90" formatToPhone("11987654321"); // "(11) 98765-4321"

Dica

Para projeto profissional que tem múltiplos documentos, brazilian-valueseconomiza tempo e evita bugs sutis. Pacote pequeno (~20 KB), validado, mantido. Alternativas: cpf-cnpj-validator (foca em CPF/CNPJ), @fnando/cpf(minimalista).

Backend: validação via Zod

import { z } from "zod"; import { isCPF, isCNPJ, isCEP } from "brazilian-values"; const formSchema = z.object({ nome: z.string().min(2), email: z.string().email(), cpf: z .string() .transform((s) => s.replace(/\D/g, "")) .refine((s) => s.length === 11, { message: "CPF deve ter 11 dígitos" }) .refine(isCPF, { message: "CPF inválido" }), cnpj: z .string() .optional() .refine((s) => !s || isCNPJ(s), { message: "CNPJ inválido" }), cep: z .string() .refine(isCEP, { message: "CEP inválido" }), telefone: z .string() .refine((s) => validarTelefoneBR(s).valido, { message: "Telefone inválido" }), }); // No handler da rota export async function POST(req: Request) { const body = await req.json(); const result = formSchema.safeParse(body); if (!result.success) { return Response.json( { errors: result.error.flatten() }, { status: 400 }, ); } // Dados validados em result.data await salvarNoBanco(result.data); return Response.json({ ok: true }); }

UX em formulários brasileiros

Feedback em tempo real vs onBlur

  • Validação enquanto digita (errada): mostra “inválido” com 3 dígitos. Frustra.
  • Validação onBlur (recomendada): usuário termina de digitar, sai do campo, então valida. Experiência natural.
  • Validação no submit: muito tarde — usuário já preencheu tudo. Use como fallback, não como principal.

Ordem dos campos

Padrão brasileiro:

  1. Nome
  2. E-mail
  3. CPF (ou CNPJ)
  4. Telefone (celular primeiro, fixo depois se houver)
  5. CEP (que preenche endereço)
  6. Rua, número, complemento (autocompletados pelo CEP)
  7. Bairro, cidade, UF (autocompletados)

Mensagens de erro claras

  • Bom: “CPF inválido. Verifique os dígitos.”
  • Ruim: “Erro”, “Dado inválido”, “Regex falhou”.

Inputmode para teclado mobile correto

<input type="tel" inputMode="numeric" /> // CPF, CNPJ, telefone <input type="email" inputMode="email" /> // e-mail <input type="text" inputMode="text" /> // nome, endereço

No mobile, inputMode=“numeric” mostra teclado numérico direto — economiza 2 toques por campo, multiplica por 5 campos = menos atrito.

Armazenando corretamente no banco

Como armazenar cada documento
CritérioFormato recomendadoExemplo
CPF11 dígitos (string)"12345678909"
CNPJ14 dígitos (string)"12345678000190"
CEP8 dígitos (string)"01001000"
Telefone+5511987654321 (E.164)"+5511987654321"
PIS11 dígitos (string)"12345678901"

Em PostgreSQL, use VARCHAR(11) para CPF e VARCHAR(14) para CNPJ. Evite BIGINT — CPF começando com zero perde o zero.

Erros clássicos que quebram validação

  • Não normalizar antes de validar: “123.456.789-09” com regex de 11 dígitos falha.
  • Não rejeitar sequências repetidas: 000.000.000-00 passa no módulo 11 ingenuamente implementado.
  • Validar apenas no frontend: vulnerável a ataques diretos.
  • Armazenar com formatação: duplica armazenamento, dificulta queries.
  • Regex de telefone que exige formatação: quebra quando usuário cola apenas dígitos.
  • Não aceitar DDD sem parênteses: frustra usuário.
  • Preenchimento de endereço que não permite edição: bug clássico; usuário precisa editar quando CEP retorna dado incorreto.
  • CPF obrigatório para estrangeiros: considere opção de passaporte ou CPF alfanumérico (novo padrão).

Integração end-to-end: componente React completo

"use client"; import { useState } from "react"; import { IMaskInput } from "react-imask"; import { isCPF, isCEP, formatToCPF } from "brazilian-values"; import { buscarCEP } from "@/lib/via-cep"; export function FormularioCadastro() { const [formData, setFormData] = useState({ nome: "", email: "", cpf: "", cep: "", endereco: "", numero: "", cidade: "", uf: "", }); const [errors, setErrors] = useState<Record<string, string>>({}); const [submitting, setSubmitting] = useState(false); const validateField = (field: string, value: string) => { const newErrors = { ...errors }; delete newErrors[field]; if (field === "cpf") { if (value && !isCPF(value)) { newErrors.cpf = "CPF inválido"; } } if (field === "cep") { if (value && !isCEP(value)) { newErrors.cep = "CEP inválido"; } } if (field === "email") { if (value && !/^[\w.+-]+@[\w-]+\.[\w-]+$/.test(value)) { newErrors.email = "E-mail inválido"; } } setErrors(newErrors); }; const handleCepBlur = async () => { validateField("cep", formData.cep); if (!isCEP(formData.cep)) return; const endereco = await buscarCEP(formData.cep); if (endereco) { setFormData((prev) => ({ ...prev, endereco: endereco.logradouro, cidade: endereco.localidade, uf: endereco.uf, })); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); const res = await fetch("/api/cadastrar", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData), }); if (!res.ok) { const { errors: serverErrors } = await res.json(); setErrors(serverErrors.fieldErrors); } setSubmitting(false); }; return ( <form onSubmit={handleSubmit}> <input placeholder="Nome completo" value={formData.nome} onChange={(e) => setFormData({ ...formData, nome: e.target.value })} /> <input type="email" inputMode="email" placeholder="E-mail" value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} onBlur={() => validateField("email", formData.email)} aria-invalid={!!errors.email} /> {errors.email && <span>{errors.email}</span>} <IMaskInput mask="000.000.000-00" value={formData.cpf} onAccept={(val: string) => setFormData({ ...formData, cpf: val })} onBlur={() => validateField("cpf", formData.cpf)} placeholder="CPF" /> {errors.cpf && <span>{errors.cpf}</span>} <IMaskInput mask="00000-000" value={formData.cep} onAccept={(val: string) => setFormData({ ...formData, cep: val })} onBlur={handleCepBlur} placeholder="CEP" /> {errors.cep && <span>{errors.cep}</span>} <input placeholder="Endereço" value={formData.endereco} onChange={(e) => setFormData({ ...formData, endereco: e.target.value })} /> <input placeholder="Número" value={formData.numero} onChange={(e) => setFormData({ ...formData, numero: e.target.value })} /> <input placeholder="Cidade" value={formData.cidade} onChange={(e) => setFormData({ ...formData, cidade: e.target.value })} /> <input placeholder="UF" maxLength={2} value={formData.uf} onChange={(e) => setFormData({ ...formData, uf: e.target.value.toUpperCase() })} /> <button type="submit" disabled={submitting || Object.keys(errors).length > 0}> {submitting ? "Enviando..." : "Cadastrar"} </button> </form> ); }

Acessibilidade em formulários brasileiros

  • Label sempre associado ao input: <label htmlFor> + <input id>.
  • Erros acessíveis: aria-invalid=“true” + aria-describedby apontando para o span de erro.
  • Não dependa apenas de cor: ícone X ao lado do campo, texto claro.
  • Contraste de erro: vermelho WCAG AA (4.5:1).
  • Botão submit não desabilita sem feedback: mostre motivo (“Complete os campos obrigatórios”).

Segurança em formulários

  • Rate limit: evita ataques de cadastro em massa.
  • CAPTCHA em formulários públicos: reCAPTCHA v3 (invisível) ou Cloudflare Turnstile.
  • CSRF token: obrigatório em formulários autenticados.
  • HTTPS: dados pessoais sempre via conexão segura.
  • Sanitize HTML no backend: prevê XSS armazenado.
  • Normalize e valide server-side: regra de ouro.

Validação em uma frase

Validação de documentos brasileiros em formulários exige conhecimento específico — módulo 11 para CPF e CNPJ, estrutura de DDD + 9 para telefone celular, integração com ViaCEP para endereço. Combinado com máscara amigável, validação dupla (frontend + backend), normalização consistente e mensagens de erro claras, o resultado é formulário que converte bem e não frustra usuário.

Perguntas frequentes

Preciso validar documentos no frontend e no backend?+

Nos dois. Frontend dá feedback imediato ao usuário (UX), backend é a validação confiável (segurança). Cliente pode desabilitar JavaScript, manipular DOM ou enviar request via curl direto. Nunca confie só em validação client-side para regras críticas como CPF ou CNPJ — ataques direcionados passam por validação frontend sem dificuldade.

Máscara de input melhora ou atrapalha a experiência?+

Melhora, quando bem feita. Máscara formata enquanto usuário digita (000.000.000-00 aparece sozinho), reduz erro e acelera preenchimento. Mal feita (bloqueia colar de clipboard, não permite correção) frustra. Bibliotecas como IMask e React Input Mask resolvem a maioria dos casos. Sempre deixe o usuário colar valor formatado ou sem formato — sua máscara se ajusta.

Qual regex para CPF, CNPJ e CEP?+

CPF: /^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$/. CNPJ: /^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$/. CEP: /^\d{5}-?\d{3}$/. Telefone celular: /^\(?\d{2}\)?\s?9?\d{4,5}-?\d{4}$/. Importante: regex valida formato, mas CPF e CNPJ também precisam validar dígito verificador via algoritmo de módulo 11 — regex sozinha aceita 000.000.000-00, que é inválido na Receita Federal.

Como validar telefone brasileiro que aceita fixo e celular?+

Celular tem 11 dígitos (incluindo DDD), começando com 9 no nono dígito. Fixo tem 10 dígitos (DDD + 4 dígitos iniciais + 4 finais). Regex genérica: /^\(?\d{2}\)?\s?9?\d{4,5}-?\d{4}$/. Para validação mais rigorosa, verifique após o DDD: se o nono dígito é 9, é celular; se não, é fixo. Use bibliotecas como libphonenumber-js para validação internacional que inclui BR.

Devo permitir que o usuário digite sem formatação?+

Sim. O usuário pode colar CPF &ldquo;12345678909&rdquo; (sem ponto nem hífen) ou &ldquo;123.456.789-09&rdquo;. Normalize no backend antes de validar: remova tudo que não é dígito (.replace(/\D/g, '')). Ao exibir, reaplique formatação. Armazenar no banco: apenas os 11 dígitos do CPF (ou 14 do CNPJ) — economiza 20-30% de espaço e elimina ambiguidade.

Como integrar busca de CEP com preenchimento automático de endereço?+

API pública dos Correios via ViaCEP (viacep.com.br/ws/01001000/json) retorna logradouro, bairro, cidade, UF a partir do CEP. Implementação: usuário digita CEP completo (8 dígitos), onBlur dispara fetch à API, preenche automaticamente os campos de endereço. Reduz digitação em 60-80%. Alternativas: AwesomeAPI, BrasilAPI — todos grátis e com SLA razoável.

PIS, CPF e CNPJ compartilham algoritmo de validação?+

Parcialmente. Todos usam módulo 11 para calcular dígito verificador, mas com pesos diferentes: CPF tem 9 dígitos + 2 verificadores; CNPJ tem 12 + 2; PIS/PASEP tem 10 + 1. Biblioteca como brazilian-values oferece validadores prontos para os três. Implementar cada um individualmente requer conhecer os pesos específicos — fácil de errar em código próprio.

O que fazer quando o usuário digita CPF inválido no mobile?+

Feedback contextual. Mostre erro apenas onBlur (após sair do campo), não em tempo real enquanto digita — senão mostra &ldquo;inválido&rdquo; com 3 dígitos. Mensagem específica: &ldquo;CPF inválido&rdquo; ou &ldquo;Verifique os números digitados&rdquo;. Ícone visual (X vermelho) junto com a mensagem ajuda. Para acessibilidade, use aria-invalid=&ldquo;true&rdquo; e aria-describedby apontando para o span de erro.

#validação#cpf#cnpj#cep#telefone#formulário#javascript#react#brasil#máscara

Artigos relacionados