Validar CNH em JavaScript: algoritmo DENATRAN passo a passo
Validar CNH em JavaScript: algoritmo DENATRAN passo a passo
O número de registro da CNH é tratado por muitos sistemas como texto livre. Isso deixa passar documentos inválidos, fantasmas ou digitados errado. A validação correta exige reproduzir o algoritmo que o DENATRAN (hoje SENATRAN) usa para gerar os dois dígitos verificadores. Este artigo mostra o algoritmo passo a passo e uma implementação TypeScript pronta para produção.
O número de registro da CNH tem estrutura definida por norma DENATRAN
O DENATRAN estabeleceu o formato do número de registro na Portaria 1.606/1998, depois consolidado em resoluções do CONTRAN. A estrutura é fixa: 11 dígitos numéricos, sem letras, sem pontuação obrigatória no campo de banco de dados.
O que é o RENACH e por que ele importa para validação
O RENACH (Registro Nacional de Carteiras de Habilitação) unifica os dados de habilitação de todos os estados. Cada registro recebe um identificador composto pelo código de UF (dois caracteres) seguido de 9 dígitos numéricos. O número que aparece no campo "Número de Registro" da CNH física corresponde apenas à parte numérica do RENACH, sem o prefixo de UF. Para validar o segundo dígito verificador com precisão, o estado emissor precisa ser conhecido. Quando o sistema armazena apenas os 11 dígitos, é possível validar o primeiro verificador com certeza e o segundo condicionalmente.
Composição dos 11 dígitos: prefixo de UF, sequencial e dois dígitos verificadores
Posições: 0 1 2 3 4 5 6 7 8 9 10
Conteúdo: [d0 d1 d2 d3 d4 d5 d6 d7 d8 D1 D2]
\_________base 9 dígitos_______/ DV1 DV2Posições 0–8 formam a base numérica atribuída pelo DETRAN estadual. D1 é o primeiro dígito verificador e D2 o segundo. O índice do estado emissor (DSC — Dígito de Segurança de Controle) varia de 1 (AC) a 27 (TO) e participa do cálculo de D2.
Diferença entre número de registro, espelho e código de segurança
O número de registro (11 dígitos) é o identificador primário. O número de espelho identifica a emissão física da CNH — pode mudar quando o documento é reimpresso sem alteração do RENACH. O código de segurança é um token de consulta no portal SENATRAN, sem algoritmo público de verificação. Para validação de formulários e APIs, só o número de registro interessa.
O algoritmo oficial usa mod-11 em duas passagens sequenciais
O mod-11 é o mesmo padrão do CPF e do CNPJ: soma ponderada dos dígitos, divisão por 11, e o resto determina o verificador. A CNH aplica duas passagens sobre os mesmos 9 dígitos base, com pesos diferentes.
Passagem 1: pesos 2 a 10 sobre os 9 primeiros dígitos
| Posição | Peso | Produto |
|---|---|---|
| 0 | 2 | d0 × 2 |
| 1 | 3 | d1 × 3 |
| 2 | 4 | d2 × 4 |
| 3 | 5 | d3 × 5 |
| 4 | 6 | d4 × 6 |
| 5 | 7 | d5 × 7 |
| 6 | 8 | d6 × 8 |
| 7 | 9 | d7 × 9 |
| 8 | 10 | d8 × 10 |
Soma todos os produtos. Calcula rem = soma % 11.
Regras de resto: quando o DSC (Dígito de Segurança de Controle) é zero
Se rem < 2 (resto 0 ou 1), D1 = 0. Caso contrário, D1 = 11 - rem. O resultado sempre cabe em um dígito — não há caso especial de "10" nessa passagem.
Passagem 2: pesos 2 a 11 incluindo o primeiro dígito verificador
A segunda passagem usa os 9 dígitos base mais D1, com pesos 2 a 11: posições 0–8 recebem pesos 2–10 (idênticos à passagem 1), e D1 recebe peso 11. O DSC do estado emissor é somado ao total antes do mod-11.
Como o estado emissor afeta o segundo cálculo (DSC ≥ 10)
O índice de estado vai de 1 a 27. Quando (total + DSC) % 11 produz 11 - rem = 10, o resultado é ajustado para 0, evitando um verificador de dois dígitos. Estados com DSC ≥ 10 (a partir de MA=10) têm maior probabilidade de acionar esse ajuste.
Implementação do primeiro dígito verificador em TypeScript
Função calcPrimeiroDV(digits: string): number
function calcPrimeiroDV(digits: string): number {
let sum = 0;
for (let i = 0; i < 9; i++) {
sum += Number(digits[i]) * (2 + i); // pesos 2, 3, ..., 10
}
const rem = sum % 11;
return rem < 2 ? 0 : 11 - rem;
}A função recebe uma string de exatamente 9 caracteres numéricos. Validação de entrada fica na função principal.
Tratamento do caso em que o resto é 0 ou 1
rem < 2 cobre os dois casos de uma vez. Diferente do CPF, a CNH não distingue entre resto 0 e resto 1 — ambos colapsam para zero. A expressão ternária é suficiente.
Testes unitários com assert puro, sem framework
import assert from "node:assert/strict";
assert.strictEqual(calcPrimeiroDV("000000000"), 0); // rem=0 → D1=0
assert.strictEqual(calcPrimeiroDV("111111111"), 11 - (99 % 11)); // rem calculado
console.log("calcPrimeiroDV: OK");Implementação do segundo dígito verificador em TypeScript
Função calcSegundoDV(digits: string, primeiroDV: number, dsc: number): number
function calcSegundoDV(
digits: string,
primeiroDV: number,
dsc: number
): number {
let sum = 0;
for (let i = 0; i < 9; i++) {
sum += Number(digits[i]) * (2 + i); // pesos 2..10
}
sum += primeiroDV * 11; // peso 11 para D1
const rem = (sum + dsc) % 11;
if (rem < 2) return 0;
const result = 11 - rem;
return result >= 10 ? 0 : result;
}Regra especial quando DSC atinge 10 ou 11
rem pode valer 0 a 10 (resultado do mod-11). Quando rem = 1, 11 - rem = 10 — um valor inválido como dígito único. O guard result >= 10 ? 0 : result garante que nunca se retorna dois dígitos. Estados com DSC alto deslocam o módulo para essa faixa com mais frequência.
Como o índice do estado (posição 1 do RENACH) entra no cálculo
const UF_DSC: Record<string, number> = {
AC: 1, AL: 2, AM: 3, AP: 4, BA: 5,
CE: 6, DF: 7, ES: 8, GO: 9, MA: 10,
MG: 11, MS: 12, MT: 13, PA: 14, PB: 15,
PE: 16, PI: 17, PR: 18, RJ: 19, RN: 20,
RO: 21, RR: 22, RS: 23, SC: 24, SE: 25,
SP: 26, TO: 27,
};Quando o UF não está disponível, passe dsc = 0 e valide apenas D1. A maioria dos formulários coleta o estado, então o UF geralmente está disponível como contexto.
Função principal validarCNH com todos os guards de entrada
Rejeitar strings não numéricas e comprimento diferente de 11
function stripMask(input: string): string {
return input.replace(/\D/g, "");
}
function validarCNH(
input: string,
uf?: string
): { valido: boolean; motivo?: string } {
const cnh = stripMask(input);
if (cnh.length !== 11) {
return { valido: false, motivo: "Comprimento inválido" };
}
const TRIVIAIS = Array.from({ length: 10 }, (_, i) => String(i).repeat(11));
if (TRIVIAIS.includes(cnh)) {
return { valido: false, motivo: "Sequência trivial" };
}
const base = cnh.slice(0, 9);
const d1Informado = Number(cnh[9]);
const d2Informado = Number(cnh[10]);
const d1Calculado = calcPrimeiroDV(base);
if (d1Informado !== d1Calculado) {
return { valido: false, motivo: "Primeiro dígito verificador inválido" };
}
if (uf) {
const dsc = UF_DSC[uf.toUpperCase()] ?? 0;
const d2Calculado = calcSegundoDV(base, d1Calculado, dsc);
if (d2Informado !== d2Calculado) {
return { valido: false, motivo: "Segundo dígito verificador inválido" };
}
}
return { valido: true };
}Rejeitar sequências triviais (00000000000 a 99999999999 repetidos)
O array TRIVIAIS cobre todas as CNHs formadas por um único dígito repetido. Esse guard é crítico: 00000000000 passa pelo algoritmo mod-11 (soma zero, rem zero, D1 zero, D2 zero), mas não representa um registro real.
Retornar objeto estruturado { valido: boolean; motivo?: string }
O campo motivo aparece apenas quando valido: false. O chamador diferencia "D1 errado" de "comprimento incorreto" sem depender de exceções ou comparar strings de erro.
Casos de borda que quebram implementações ingênuas
CNHs com prefixo de estado 0 (emissões antigas)
Emissões anteriores à padronização do RENACH usavam 0 como primeiro dígito da parte numérica. O cálculo de D1 é idêntico — a fórmula não muda. A confusão aparece ao tentar inferir o estado a partir do número sem o prefixo de UF. A solução é sempre armazenar o UF junto ao número de registro.
Segundo verificador resultando em 10 antes do ajuste
Se 11 - rem = 10, a função retorna 0. Implementações que retornam 10 diretamente falharão ao comparar com o dígito armazenado na CNH real. O guard result >= 10 ? 0 : result previne isso de forma explícita.
Entrada com máscara (pontos, hífens, espaços)
stripMask remove qualquer caractere não numérico antes de processar. Isso aceita "012.345.678-90" ou "012 345 678 90" sem erro. A verificação de comprimento ocorre após o strip.
DICA: Aplique stripMask antes de qualquer verificação de formato. Sistemas legados frequentemente persistem CNHs formatadas, e isso quebra validações que assumem entrada limpa.Suite de testes com CNHs reais e inválidas conhecidas
Tabela de vetores de teste (número, esperado, motivo)
| CNH | UF | Esperado | Motivo |
|---|---|---|---|
00000000000 | SP | inválida | Sequência trivial |
1234567890 | SP | inválida | Comprimento 10, não 11 |
1234567890X | RJ | inválida | Caractere não numérico |
12345678901 | SP | inválida | D1 provavelmente incorreto |
| (gerado em /gerador-cnh) | SP | válida | Gerado pelo algoritmo |
Para vetores com resultado "válida" confirmado, use o gerador de CNH. Os números são fictícios: não correspondem a nenhum habilitado real e são seguros para fixtures e seeds de banco.
Como gerar CNHs válidas para testes sem expor dados reais
O gerador de CNH do FakeForge aplica o mesmo algoritmo DENATRAN descrito neste artigo. Use os números gerados em dados de teste, mocks de API e seeds de desenvolvimento. Eles não passam em consultas ao portal SENATRAN — são inválidos no mundo real, válidos em formato.
Executar os testes com node --test nativo (Node 20+)
// cnh.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { validarCNH } from "./cnh.js";
test("rejeita sequência trivial", () => {
assert.deepEqual(validarCNH("00000000000"), {
valido: false,
motivo: "Sequência trivial",
});
});
test("rejeita comprimento errado", () => {
assert.deepEqual(validarCNH("1234567890"), {
valido: false,
motivo: "Comprimento inválido",
});
});# TypeScript direto com tsx (Node 20+)
node --import tsx --test cnh.test.tsO runner nativo do Node 20 não exige Jest, Vitest ou Mocha.
Integrar a validação em formulários React e em APIs Next.js
Hook useCNH com debounce para campo controlado
import { useState, useEffect } from "react";
import { validarCNH } from "@/lib/validators/cnh";
export function useCNH(uf?: string) {
const [valor, setValor] = useState("");
const [resultado, setResultado] = useState<{
valido: boolean;
motivo?: string;
} | null>(null);
useEffect(() => {
const timer = setTimeout(() => {
if (stripMask(valor).length >= 11) {
setResultado(validarCNH(valor, uf));
} else {
setResultado(null);
}
}, 400);
return () => clearTimeout(timer);
}, [valor, uf]);
return { valor, setValor, resultado };
}O debounce de 400ms evita revalidar a cada tecla. O resultado zera enquanto o campo tem menos de 11 dígitos.
Validação no route handler /api/validate/cnh com retorno padronizado
// src/app/api/validate/cnh/route.ts
import { NextRequest, NextResponse } from "next/server";
import { validarCNH } from "@/lib/validators/cnh";
export function GET(req: NextRequest) {
const numero = req.nextUrl.searchParams.get("numero") ?? "";
const uf = req.nextUrl.searchParams.get("uf") ?? undefined;
const resultado = validarCNH(numero, uf);
return NextResponse.json(resultado, { status: resultado.valido ? 200 : 422 });
}Status 422 (Unprocessable Entity) sinaliza que a entrada chegou íntegra mas falhou na validação. Siga o mesmo padrão do validador de CPF e do validador de CNPJ para consistência de contrato.
Reutilizar a mesma lógica no cliente e no servidor sem duplicação
validarCNH em @/lib/validators/cnh.ts é uma função pura sem dependências de browser ou Node.js. Importar no hook e no route handler é suficiente — não há necessidade de serializar via fetch para validação síncrona no cliente.
Considerações LGPD para sistemas que coletam número de CNH
CNH é dado pessoal sob LGPD Art. 5º, I — exige base legal (Art. 7º, IX)
O número de registro da CNH identifica uma pessoa física de forma direta (LGPD Art. 5º, I). Coletar e armazenar esse dado exige base legal explícita. A mais comum para marketplaces e sistemas B2B é execução de contrato ou legítimo interesse (LGPD Art. 7º, IX), com balancing test documentado.
AVISO: Armazenar o número de CNH "por precaução" sem finalidade específica viola o princípio da necessidade (LGPD Art. 6º, III). Se o sistema só precisa confirmar que o usuário tem habilitação, armazene um booleano ou a data de validade, não o número completo.
Princípio da minimização: validar no frontend, nunca armazenar se não for necessário
A validação de formato não exige persistência. Valide no frontend com useCNH, confirme no backend com o route handler, e persista o número apenas se ele for necessário para a finalidade declarada ao titular.
Diferença entre validar formato e cruzar com base DENATRAN/RENACH real
Este algoritmo confirma que os dígitos verificadores são matematicamente corretos. Não confirma que a CNH existe no cadastro SENATRAN, que a habilitação está ativa, nem que os dados pertencem ao usuário logado. Para confirmação real, é necessário acesso à API do SENATRAN (restrita a conveniados). O algoritmo aqui descarta entradas obviamente inválidas sem custo de I/O.
Resumo
- O número de registro da CNH tem 11 dígitos: 9 de base e 2 verificadores calculados com mod-11 em duas passagens sequenciais.
- Primeiro verificador usa pesos 2–10 sobre os 9 dígitos base; qualquer resto menor que 2 produz zero.
- Segundo verificador usa pesos 2–11 (incluindo D1) mais o índice DSC do estado emissor; resultado ≥ 10 vira zero.
validarCNH(input, uf?)retorna{ valido: boolean; motivo?: string }— o mesmo contrato serve no hook React e no route handler Next.js, sem duplicação.- Rejeite sequências triviais explicitamente:
00000000000passa pelo mod-11 por construção e precisa de guard dedicado. - Para gerar CNHs fictícias válidas em fixtures e seeds de banco, use o gerador de CNH. Para padrões análogos em outros documentos, veja CPF, CNPJ e PIS/PASEP.
Perguntas frequentes
Se não tenho o UF disponível no formulário, consigo validar a CNH parcialmente?+
Sim. Passe `uf: undefined` e valide apenas D1. Funciona, mas é menos seguro — D2 fica inconcluso. Se o formulário coleta endereço ou estado, capture-o antes de validar. Sem UF, você rejeita a maioria das entradas inválidas, mas passa em alguns casos que SENATRAN recusaria.
Gerei uma CNH no seu site, passei pra meu código e falhou. Como debugo?+
Confirme que `stripMask` removeu máscara. Verifique que pesos em `calcPrimeiroDV` são 2–10 e em `calcSegundoDV` são 2–11 mais peso 11 para D1. Reproduza manualmente: base (9 dígitos) + D1 + D2, aplique mod-11, compare com o informado. Diferença apontará erro no algoritmo.
Validar 1 milhão de CNHs é custoso? Preciso cache?+
Não. Mod-11 é O(1) — 9 multiplicações por validação. 1 milhão em ~50ms em Node.js puro. Sem cache necessário. Se precisar mais velocidade, paralelizar com Worker Threads. CPU-bound, não I/O-bound.
Existe uma API pública e gratuita de SENATRAN para confirmar se a CNH é real?+
Não. SENATRAN restringe acesso a conveniados (bancos, seguradoras, licenciados). Este algoritmo valida apenas formato matemático, não existência. Para produção com usuários reais, negocie convênio direto com SENATRAN ou delegue a parceiro credenciado.
Por que o estado não está embutido nos 11 dígitos se ele é necessário para calcular D2?+
O RENACH completo é UF+11 dígitos (ex: SP000000001). A CNH impressa mostra só os 11 numéricos — norma DENATRAN separou propositalmente. O estado é contexto da emissão, não identificador impresso. Por isso deve vir de outro campo do formulário ou contexto.