Validar CPF em Node.js e Express: Middleware com Zod
Validação de CPF em Node.js com Express, usando middleware dedicado e Zod para validação de schema. A lógica segue o algoritmo de módulo 11. Este artigo foca na integração com o ecossistema Node.
Função de validação
function isValidCpf(value) {
const cpf = value.replace(/\D/g, "");
if (cpf.length !== 11) return false;
if (/^(\d)\1{10}$/.test(cpf)) return false;
for (let t = 9; t < 11; t++) {
let sum = 0;
for (let c = 0; c < t; c++) {
sum += Number(cpf[c]) * ((t + 1) - c);
}
const digit = ((10 * sum) % 11) % 10;
if (Number(cpf[t]) !== digit) return false;
}
return true;
}Mesma lógica da implementação JavaScript pura, extraída como função reutilizável para o back-end.
Middleware Express
function validateCpf(field) {
return (req, res, next) => {
const value = req.body[field];
if (!value || typeof value !== "string") {
return res.status(400).json({
errors: [{ field, message: "CPF é obrigatório" }],
});
}
if (!isValidCpf(value)) {
return res.status(400).json({
errors: [{ field, message: "CPF inválido" }],
});
}
// Normaliza para apenas dígitos
req.body[field] = value.replace(/\D/g, "");
next();
};
}Análise do código
isValidCpf é uma função pura sem dependências do Express. O middleware validateCpf recebe o nome do campo como parâmetro, extrai o valor de req.body, valida e normaliza (remove pontuação) antes de passar para o próximo handler com next().
A normalização no middleware garante que o controller sempre receba o CPF como 11 dígitos, independente do formato enviado pelo cliente.
Uso com Express
import express from "express";
const app = express();
app.use(express.json());
app.post("/api/pessoas", validateCpf("cpf"), (req, res) => {
// req.body.cpf já está validado e normalizado (apenas dígitos)
res.status(201).json({ cpf: req.body.cpf });
});Integração com Zod
Para projetos que já usam Zod, a validação de CPF encaixa como refine no schema:
import { z } from "zod";
const cpfSchema = z
.string()
.transform((v) => v.replace(/\D/g, ""))
.refine((v) => v.length === 11, "CPF deve ter 11 dígitos")
.refine((v) => !/^(\d)\1{10}$/.test(v), "CPF inválido")
.refine((v) => {
for (let t = 9; t < 11; t++) {
let sum = 0;
for (let c = 0; c < t; c++) {
sum += Number(v[c]) * ((t + 1) - c);
}
if (Number(v[t]) !== ((10 * sum) % 11) % 10) return false;
}
return true;
}, "CPF inválido");
const pessoaSchema = z.object({
nome: z.string().min(1),
cpf: cpfSchema,
email: z.string().email(),
});Middleware genérico para validar qualquer schema Zod:
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
errors: result.error.issues.map((i) => ({
field: i.path.join("."),
message: i.message,
})),
});
}
req.body = result.data;
next();
};
}
app.post("/api/pessoas", validate(pessoaSchema), (req, res) => {
res.status(201).json(req.body);
});Integração com express-validator
Alternativa usando express-validator com validador customizado:
import { body, validationResult } from "express-validator";
const cpfValidation = body("cpf")
.notEmpty().withMessage("CPF é obrigatório")
.custom((value) => {
if (!isValidCpf(value)) throw new Error("CPF inválido");
return true;
});
app.post("/api/pessoas", cpfValidation, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.status(201).json({ cpf: req.body.cpf });
});Testes
import { describe, it, expect } from "vitest";
describe("isValidCpf", () => {
it("retorna true para CPF válido com máscara", () => {
expect(isValidCpf("529.982.247-25")).toBe(true);
});
it("retorna true para CPF válido sem máscara", () => {
expect(isValidCpf("52998224725")).toBe(true);
});
it("retorna false para CPF com dígitos repetidos", () => {
expect(isValidCpf("111.111.111-11")).toBe(false);
});
it("retorna false para dígito verificador incorreto", () => {
expect(isValidCpf("529.982.247-26")).toBe(false);
});
it("retorna false para tamanho incorreto", () => {
expect(isValidCpf("123.456.789")).toBe(false);
});
it("valida CPF com zeros à esquerda", () => {
expect(isValidCpf("000.000.001-91")).toBe(true);
});
});
describe("Zod cpfSchema", () => {
it("aceita CPF válido e normaliza", () => {
const result = cpfSchema.safeParse("529.982.247-25");
expect(result.success).toBe(true);
if (result.success) expect(result.data).toBe("52998224725");
});
it("rejeita CPF inválido", () => {
const result = cpfSchema.safeParse("111.111.111-11");
expect(result.success).toBe(false);
});
});Use o gerador de CPF válido para criar números de teste em lote.
Veja também: validar CPF em JavaScript puro.