Validando CPF e CNPJ em Node.js e TypeScript sem bibliotecas externas
Implemente validadores robustos de CPF e CNPJ do zero em TypeScript, com tratamento de casos especiais, testes unitários e integração com formulários React.
10 de abril de 2025 · 5 min de leitura
Por que implementar do zero?
Bibliotecas como cpf-cnpj-validator e validate-br são populares e funcionam bem. No entanto, entender o algoritmo subjacente traz vantagens práticas:
- Sem dependência externa: uma biblioteca a menos para auditar, atualizar e monitorar por vulnerabilidades.
- Personalização: você pode adaptar a validação para casos de negócio específicos (ex.: aceitar apenas CPFs de determinadas regiões fiscais).
- Portabilidade: a mesma lógica pode ser reescrita para qualquer linguagem sem pesquisar uma nova biblioteca.
- Conhecimento: fundamental para entrevistas técnicas e code reviews.
Validação de CPF
A lógica completa
// src/lib/validators/cpf.ts
const INVALID_SEQUENCES = new Set([
"00000000000", "11111111111", "22222222222",
"33333333333", "44444444444", "55555555555",
"66666666666", "77777777777", "88888888888",
"99999999999",
]);
export function sanitizeCpf(cpf: string): string {
return cpf.replace(/\D/g, "");
}
export function formatCpf(cpf: string): string {
const s = sanitizeCpf(cpf);
return s.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
}
function calcVerifier(digits: string, length: number): number {
const sum = digits
.slice(0, length)
.split("")
.reduce((acc, d, i) => acc + Number(d) * (length + 1 - i), 0);
const rem = (sum * 10) % 11;
return rem >= 10 ? 0 : rem;
}
export function isValidCpf(cpf: string): boolean {
const digits = sanitizeCpf(cpf);
if (digits.length !== 11) return false;
if (INVALID_SEQUENCES.has(digits)) return false;
return (
calcVerifier(digits, 9) === Number(digits[9]) &&
calcVerifier(digits, 10) === Number(digits[10])
);
}
Testes unitários com Vitest
// src/lib/validators/cpf.test.ts
import { describe, it, expect } from "vitest";
import { isValidCpf, formatCpf } from "./cpf";
describe("isValidCpf", () => {
it("aceita CPF válido formatado", () => {
expect(isValidCpf("123.456.789-09")).toBe(true);
});
it("aceita CPF válido sem formatação", () => {
expect(isValidCpf("12345678909")).toBe(true);
});
it("rejeita CPF com dígito verificador errado", () => {
expect(isValidCpf("123.456.789-00")).toBe(false);
});
it("rejeita sequências repetidas", () => {
expect(isValidCpf("111.111.111-11")).toBe(false);
expect(isValidCpf("000.000.000-00")).toBe(false);
});
it("rejeita CPF com menos de 11 dígitos", () => {
expect(isValidCpf("1234567890")).toBe(false);
});
it("rejeita string vazia", () => {
expect(isValidCpf("")).toBe(false);
});
});
describe("formatCpf", () => {
it("formata corretamente 11 dígitos", () => {
expect(formatCpf("12345678909")).toBe("123.456.789-09");
});
it("reformata CPF já formatado", () => {
expect(formatCpf("123.456.789-09")).toBe("123.456.789-09");
});
});
Validação de CNPJ
O CNPJ segue a mesma lógica de módulo 11, mas com pesos diferentes e 14 dígitos.
Estrutura do CNPJ
NN.NNN.NNN/SSSS-DD
│ │ └─ 2 dígitos verificadores
│ └───── 4 dígitos do número de ordem (filial)
└─────────────── 8 dígitos do número base (CNPJ raiz)
Implementação
// src/lib/validators/cnpj.ts
const INVALID_CNPJ_SEQUENCES = Array.from({ length: 10 }, (_, i) =>
String(i).repeat(14)
);
export function sanitizeCnpj(cnpj: string): string {
return cnpj.replace(/\D/g, "");
}
export function formatCnpj(cnpj: string): string {
const s = sanitizeCnpj(cnpj);
return s.replace(
/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/,
"$1.$2.$3/$4-$5"
);
}
function calcCnpjVerifier(digits: string, length: number): number {
const weights =
length === 12
? [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
: [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
const sum = digits
.slice(0, length)
.split("")
.reduce((acc, d, i) => acc + Number(d) * weights[i], 0);
const rem = sum % 11;
return rem < 2 ? 0 : 11 - rem;
}
export function isValidCnpj(cnpj: string): boolean {
const digits = sanitizeCnpj(cnpj);
if (digits.length !== 14) return false;
if (INVALID_CNPJ_SEQUENCES.includes(digits)) return false;
return (
calcCnpjVerifier(digits, 12) === Number(digits[12]) &&
calcCnpjVerifier(digits, 13) === Number(digits[13])
);
}
Integração com formulários React e react-hook-form
// components/DocumentInput.tsx
"use client";
import { useForm } from "react-hook-form";
import { isValidCpf } from "@/lib/validators/cpf";
import { isValidCnpj } from "@/lib/validators/cnpj";
type FormData = { document: string };
export function DocumentForm() {
const { register, handleSubmit, watch, formState: { errors } } = useForm<FormData>();
const documentValue = watch("document", "");
const isCompany = documentValue.replace(/\D/g, "").length > 11;
const onSubmit = (data: FormData) => {
console.log("Documento válido:", data.document);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">
{isCompany ? "CNPJ" : "CPF"}
</label>
<input
{...register("document", {
validate: (value) => {
const digits = value.replace(/\D/g, "");
if (digits.length <= 11) {
return isValidCpf(value) || "CPF inválido";
}
return isValidCnpj(value) || "CNPJ inválido";
},
})}
placeholder="000.000.000-00 ou 00.000.000/0001-00"
className="w-full border rounded-lg px-4 py-2"
/>
{errors.document && (
<p className="text-red-500 text-sm mt-1">
{errors.document.message}
</p>
)}
</div>
<button type="submit" className="bg-emerald-600 text-white px-4 py-2 rounded-lg">
Validar
</button>
</form>
);
}
Validação no lado do servidor com Zod
Para validar CPF/CNPJ em APIs Next.js ou tRPC, integre com Zod:
// lib/schemas/document.ts
import { z } from "zod";
import { isValidCpf } from "./validators/cpf";
import { isValidCnpj } from "./validators/cnpj";
export const cpfSchema = z
.string()
.min(11, "CPF deve ter 11 dígitos")
.refine(isValidCpf, { message: "CPF inválido" });
export const cnpjSchema = z
.string()
.min(14, "CNPJ deve ter 14 dígitos")
.refine(isValidCnpj, { message: "CNPJ inválido" });
export const documentSchema = z.union([cpfSchema, cnpjSchema]);
// app/api/validate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { documentSchema } from "@/lib/schemas/document";
export async function POST(req: NextRequest) {
const body = await req.json();
const result = documentSchema.safeParse(body.document);
if (!result.success) {
return NextResponse.json(
{ valid: false, error: result.error.issues[0].message },
{ status: 400 }
);
}
return NextResponse.json({ valid: true });
}
Conclusão
Implementar a validação de CPF e CNPJ sem bibliotecas externas é simples e tem vantagens claras em termos de controle, performance e independência de dependências. Com TypeScript, Zod e react-hook-form, a integração em aplicações Next.js modernas é direta e tipada end-to-end.
Para gerar CPFs e CNPJs fictícios para testar sua implementação, use o Gerador de CPF disponível neste site.
Precisa gerar CPFs para testar sua implementação?
Usar o Gerador de CPF →