Node.jsTypeScriptCPFCNPJValidação

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 →