Si tu app pide DNI o RUC en algún punto del flujo (KYC, onboarding, facturación, ecommerce con factura electrónica), tarde o temprano te va a tocar consultar SUNAT, RENIEC o algún proveedor que envuelva ambos.
Pero antes de gastar una llamada API por cada documento que entra, vale la pena validar offline: muchos errores son tan triviales como un dígito de menos, un espacio metido al inicio, o un usuario tipeando 12345678 para ver si pasa.
Validar localmente te ahorra latencia, dinero (si pagas por consulta) y rate limits.
¿Cómo se validan?
DNI
8 dígitos numéricos. RENIEC asigna desde 00000001 hacia arriba. La regla práctica:
- Exactamente 8 dígitos
- Sin secuencias triviales (
00000000,11111111, etc.)
function isValidDNI(dni: string): boolean {
if (!/^d{8}$/.test(dni)) return false;
if (/^(d)1{7}$/.test(dni)) return false; // todos iguales
return true;
}
Nota: existe un cálculo módulo-11 para DNI usado en sistemas bancarios peruanos, pero RENIEC no lo expone públicamente como check-digit obligatorio. Para validación pública se acepta el formato, y la verificación real ocurre contra el padrón.
RUC (la parte interesante)
11 dígitos. Inicia con un par que indica el tipo de contribuyente:
| Prefijo | Tipo |
|---|---|
| 10 | Persona natural |
| 15 | No domiciliado |
| 17 | Sucesión indivisa |
| 20 | Persona jurídica |
El último dígito es un check-digit calculado con módulo-11 sobre los primeros 10:
- Multiplica cada dígito por su peso:
[5, 4, 3, 2, 7, 6, 5, 4, 3, 2] - Suma todos los productos
expected = (11 - (sum % 11)) % 10- Compara con el último dígito del RUC
function isValidRUC(ruc: string): boolean {
if (!/^d{11}$/.test(ruc)) return false;
if (!['10', '15', '17', '20'].includes(ruc.slice(0, 2))) return false;
const weights = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
const sum = weights.reduce((acc, w, i) => acc + w * Number(ruc[i]), 0);
const expected = (11 - (sum % 11)) % 10;
return Number(ruc[10]) === expected;
}
Esto rechaza inmediatamente RUC tipeados al azar y secuencias inválidas, sin necesidad de pegarle a SUNAT.
Tipo de contribuyente
Solo con el prefijo ya sabes qué tipo de entidad es:
function tipoContribuyente(ruc: string): 'natural' | 'juridica' | 'no-domiciliado' | 'sucesion' | null {
if (!isValidRUC(ruc)) return null;
switch (ruc.slice(0, 2)) {
case '10': return 'natural';
case '15': return 'no-domiciliado';
case '17': return 'sucesion';
case '20': return 'juridica';
default: return null;
}
}
Útil para:
- Decidir si pides datos de razón social o nombre completo
- Aplicar IGV correctamente (algunas modalidades de no-domiciliado tienen tratamiento distinto)
- Mostrar diferentes flujos de KYC
La librería: dni-validator-peru
Para no copiar-pegar este código en cada proyecto, lo empaqueté como dni-validator-peru — TypeScript ESM, zero dependencies, MIT, 17 tests.
npm install dni-validator-peru
import {
isValidDNI,
isValidRUC,
validateDocumento,
tipoContribuyente,
} from 'dni-validator-peru';
isValidDNI('72345678'); // true
isValidDNI('00000000'); // false
isValidRUC('20602431216'); // true (Grupo Securex S.A.C.)
tipoContribuyente('20602431216'); // 'juridica'
// Detector unificado: te dice si es DNI, RUC o ninguno
validateDocumento('72345678'); // { tipo: 'DNI', valid: true }
validateDocumento('20602431216'); // { tipo: 'RUC', valid: true, subtipo: 'juridica' }
validateDocumento('abc'); // { tipo: null, valid: false }
Por qué la mantengo
Securex hace KYC en cada onboarding (somos casa de cambio digital regulada por SBS, certificada ISO 37301). Lo hicimos auto-fill: el usuario ingresa su DNI, y validamos módulo-11 + buscamos en padrón SUNAT. El módulo-11 elimina ~5% de submissions inválidas antes de gastar un call de API.
Esa lógica de validación local me parecía suficientemente común y aburrida como para sacarla en open source.
Otras librerías hermanas
Las tres mantengo en github.com/Edsoncame:
- tipo-cambio-peru — BCRP / SBS / SUNAT en una sola llamada.
- feriados-peru — Calendario de feriados nacionales con utilidades de días hábiles.
Todas zero-dependency, ESM, MIT.
npm · GitHub · Mantenido por Securex.
