- Blog
- Integración
Validar RUC y DNI en el frontend y en el backend
Cómo validar RUC y DNI en el frontend (formato y dígito verificador) y en el backend (existencia vía API) sin confiar en el cliente, con ejemplos en JavaScript.
El formulario de alta de clientes de tu sistema pide un RUC o un DNI. El usuario escribe, pulsa guardar y, meses después, la factura sale con un número que no existe o con una razón social que no coincide con la de SUNAT. El problema no es que faltara validación; es que estaba en un solo lugar y validaba una sola cosa.
Validar un documento de identidad tiene dos partes que se confunden a menudo: comprobar que el número está bien escrito (formato) y comprobar que corresponde a alguien real (existencia). La primera se puede hacer en el navegador, al instante y gratis. La segunda solo puede hacerla tu servidor, consultando a la fuente oficial. Y ninguna de las dos puede confiar en lo que la otra dice haber hecho.
En esta guía vas a montar las dos capas con JavaScript en ambos lados, compartiendo el mismo código de validación, y a cuidar que el usuario entienda cada error sin frustrarse.
Formato y existencia son dos validaciones distintas
El formato es lo que puedes saber mirando el número. Un RUC tiene 11 dígitos, empieza por 10, 15, 17 o 20, y su último dígito se calcula a partir de los diez anteriores. Un DNI tiene exactamente 8 dígitos. Si el texto no cumple esas reglas, no hace falta preguntar a nadie: está mal.
La existencia es lo que no puedes saber sin consultar. Un RUC puede tener formato perfecto, dígito verificador incluido, y no figurar en el padrón de SUNAT. Un DNI de 8 dígitos puede no corresponder a ninguna persona. Para resolverlo necesitas la fuente oficial, y eso ocurre en tu servidor, a través de una API como las de consulta RUC y consulta DNI de endpoint.pe.
Esta tabla resume dónde va cada cosa:
| Validación | Dónde | Costo | Qué detecta |
|---|---|---|---|
| Longitud y caracteres | Navegador y servidor | Ninguno | Dedos que se van, espacios, letras |
| Dígito verificador (RUC) | Navegador y servidor | Ninguno | Errores de tipeo que conservan la longitud |
| Existencia en el padrón | Solo servidor | Una consulta | Números bien formados que no existen |
| Estado y condición (RUC) | Solo servidor | Misma consulta | BAJA, NO HABIDO, SUSPENSIÓN |
Fíjate en que las dos primeras filas se repiten en ambos lados. No es redundancia, es la regla central de todo este artículo: el servidor no confía en que el navegador ya validó.
Validar el formato en el navegador
La validación de formato vive en un módulo sin dependencias que puedas importar tanto desde el frontend como desde Node. Así la regla se escribe una vez y no se desincroniza.
// validadores.js (se usa en el navegador y en el servidor)
export function dniTieneFormatoValido(dni) {
return /^\d{8}$/.test(dni);
}
export function rucTieneFormatoValido(ruc) {
if (!/^(10|15|17|20)\d{9}$/.test(ruc)) return false;
const pesos = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
const suma = pesos.reduce((acc, peso, i) => acc + peso * Number(ruc[i]), 0);
const resto = 11 - (suma % 11);
const digito = resto === 10 ? 0 : resto === 11 ? 1 : resto;
return digito === Number(ruc[10]);
}
export function limpiarDocumento(texto) {
return texto.replace(/\D/g, '');
}
limpiarDocumento quita todo lo que no sea dígito. Es la primera línea de defensa contra lo más común: el usuario pega "20131312955 " con un espacio al final, o escribe "20-131312955" porque así lo vio en una factura impresa. Limpia antes de validar, pero muestra al usuario lo que quedó para que no haya sorpresas.
El algoritmo del dígito verificador del RUC está explicado con ejemplos en cómo validar un RUC, y los errores típicos del DNI (ceros a la izquierda, tratarlo como número) en cómo validar un DNI en tu sistema. Aquí los damos por conocidos.
En el formulario, conecta el validador al campo. Dos atributos HTML ayudan antes de escribir una línea de JavaScript: inputmode="numeric" abre el teclado numérico en el celular, y maxlength evita que el usuario escriba de más. No uses type="number": no está pensado para identificadores, permite signos, puntos y la letra e, y puede alterar los ceros a la izquierda según el navegador.
// formulario.js (navegador)
import { dniTieneFormatoValido, rucTieneFormatoValido, limpiarDocumento } from './validadores.js';
const campo = document.querySelector('#documento');
const tipo = document.querySelector('#tipo-documento'); // "ruc" o "dni"
const ayuda = document.querySelector('#documento-ayuda');
function validarFormato() {
const valor = limpiarDocumento(campo.value);
campo.value = valor;
if (valor === '') {
ayuda.textContent = '';
return false;
}
const esValido = tipo.value === 'ruc'
? rucTieneFormatoValido(valor)
: dniTieneFormatoValido(valor);
if (!esValido) {
ayuda.textContent = tipo.value === 'ruc'
? 'El RUC debe tener 11 dígitos y empezar en 10 o 20. Revisa si hay un número cambiado.'
: 'El DNI debe tener exactamente 8 dígitos.';
} else {
ayuda.textContent = '';
}
return esValido;
}
campo.addEventListener('blur', validarFormato);
Mostrar errores sin frustrar al usuario
Cuándo mostrar el error importa tanto como qué dice. Si validas en cada tecla, el usuario ve "el RUC debe tener 11 dígitos" desde el primer dígito, y eso se siente como un regaño. Por eso el ejemplo valida en blur, cuando el usuario sale del campo. Una vez que ya mostraste un error, sí conviene revalidar en cada tecla para que el mensaje desaparezca apenas lo corrija.
Los mensajes tienen que decir qué está mal y, si se puede, qué hacer. "RUC inválido" no ayuda a nadie. "El RUC debe tener 11 dígitos, tiene 10" o "El último dígito no coincide, revisa si hay un número cambiado" sí. Cuando el dígito verificador falla, el error casi siempre es un dígito transpuesto o uno de más; decírselo ahorra un intento.
Hay un error que el frontend no debe inventar: "este RUC no existe". El navegador no lo sabe. Lo único que puede afirmar es que el formato está bien o mal. La existencia la confirma el servidor, y el mensaje correspondiente llega de vuelta como respuesta de tu endpoint.
Y un detalle de accesibilidad barato: el elemento donde escribes el mensaje de ayuda debería tener aria-live="polite" para que los lectores de pantalla lo anuncien, y el campo debería recibir aria-invalid="true" cuando hay error. Son dos atributos y mejoran la experiencia de mucha gente.
Validar la existencia en el servidor
El servidor recibe el documento, vuelve a limpiar y validar el formato con el mismo módulo, y recién entonces consulta la API. Este ejemplo usa Express y la API de endpoint.pe; el patrón es idéntico en cualquier framework y con cualquier proveedor que responda JSON.
// servidor.js (Node + Express)
import express from 'express';
import { dniTieneFormatoValido, rucTieneFormatoValido, limpiarDocumento } from './validadores.js';
const app = express();
const BASE_URL = 'https://api.endpoint.pe/v1';
async function consultar(ruta) {
const respuesta = await fetch(`${BASE_URL}${ruta}`, {
headers: { Authorization: `Bearer ${process.env.ENDPOINT_PE_TOKEN}` },
signal: AbortSignal.timeout(10_000),
});
if (respuesta.status === 404) return null;
if (!respuesta.ok) throw new Error(`API respondió HTTP ${respuesta.status}`);
return (await respuesta.json()).data;
}
app.get('/api/documento/:tipo/:numero', async (req, res) => {
const numero = limpiarDocumento(req.params.numero);
const esRuc = req.params.tipo === 'ruc';
const formatoValido = esRuc ? rucTieneFormatoValido(numero) : dniTieneFormatoValido(numero);
if (!formatoValido) {
return res.status(400).json({ error: 'formato_invalido' });
}
try {
const datos = await consultar(esRuc ? `/ruc/${numero}` : `/dni/${numero}`);
if (!datos) {
return res.status(404).json({ error: 'not_found' });
}
if (esRuc) {
return res.json({
razonSocial: datos.razon_social,
estado: datos.estado,
condicion: datos.condicion,
direccion: datos.direccion,
});
}
return res.json({
nombres: datos.nombres,
apellidoPaterno: datos.apellido_paterno,
apellidoMaterno: datos.apellido_materno,
});
} catch (error) {
console.error('Consulta falló', { tipo: req.params.tipo, numero, mensaje: error.message });
return res.status(503).json({ error: 'servicio_no_disponible' });
}
});
app.listen(3000);
Cuando la API devuelve 404, el servidor responde 404 a su vez y el frontend muestra "no existe en el padrón". Cuando devuelve datos, el servidor reenvía solo los campos que el formulario necesita, con nombres propios de tu aplicación. Y cuando algo falla (token, créditos, red), responde un 503 genérico y guarda el detalle en el log: el usuario no tiene por qué ver "sin créditos disponibles".
Para el RUC, no te quedes en "existe". Un contribuyente en BAJA DEFINITIVA o NO HABIDO existe en el padrón, pero no deberías facturarle como si nada. Devuelve estado y condicion al frontend y decide ahí (o mejor, en el propio servidor al guardar) si bloqueas, adviertes o aceptas con una marca. En consultar RUC con Node.js desarrollamos el endpoint con manejo de errores completo.
Nunca confíes en lo que llega del cliente
El navegador es territorio del usuario. Cualquiera puede abrir las herramientas de desarrollo, desactivar tu JavaScript, modificar el valor del campo después de validado o llamar directamente a tu endpoint con curl. Si el servidor asume que "ya se validó en el frontend", basta un POST a mano para guardar un RUC de tres letras.
Por eso el endpoint anterior repite limpiarDocumento y la validación de formato aunque el frontend ya lo hizo. Y por eso la validación definitiva (la que decide si el registro se guarda) ocurre al recibir el formulario completo en el servidor, no en el endpoint de consulta. El endpoint de consulta es una ayuda para el usuario; la validación al guardar es la que protege tus datos.
Esto tiene una consecuencia práctica en el diseño: el frontend puede autocompletar la razón social con lo que devolvió tu endpoint, pero al guardar, el servidor debería volver a consultar (o usar una caché reciente) y persistir el valor oficial, no el que llegó en el formulario. Si alguien editó la razón social en el campo después del autocompletado, se ignora.
Lo mismo vale para el token: nunca viaja al navegador. El frontend solo conoce la ruta de tu endpoint, y es el servidor quien pone la cabecera Authorization al hablar con la API.
Existe para que el usuario corrija rápido y para no gastar consultas en números mal escritos. Todo lo que de verdad importa (formato, existencia, estado, qué se guarda) se decide en el servidor.
Conectar los dos lados
Con el validador compartido y el endpoint listo, falta el puente: cuando el formato es válido en el navegador, consultar al servidor y mostrar el resultado. Este código extiende el formulario.js anterior.
// formulario.js (continuación)
const razonSocial = document.querySelector('#razon-social');
let ultimoConsultado = '';
async function verificarExistencia() {
if (!validarFormato()) return;
const numero = campo.value;
if (numero === ultimoConsultado) return;
ultimoConsultado = numero;
ayuda.textContent = 'Verificando...';
try {
const respuesta = await fetch(`/api/documento/${tipo.value}/${numero}`);
if (respuesta.status === 404) {
ayuda.textContent = tipo.value === 'ruc'
? 'Este RUC no figura en el padrón de SUNAT.'
: 'Este DNI no figura en RENIEC.';
return;
}
if (!respuesta.ok) {
ayuda.textContent = 'No pudimos verificar el documento. Puedes continuar y lo revisaremos después.';
return;
}
const datos = await respuesta.json();
ayuda.textContent = '';
if (tipo.value === 'ruc') {
razonSocial.value = datos.razonSocial;
if (datos.estado !== 'ACTIVO' || datos.condicion !== 'HABIDO') {
ayuda.textContent = `Atención: estado ${datos.estado}, condición ${datos.condicion}.`;
}
} else {
razonSocial.value = `${datos.nombres} ${datos.apellidoPaterno} ${datos.apellidoMaterno}`;
}
} catch {
ayuda.textContent = 'No pudimos verificar el documento. Puedes continuar y lo revisaremos después.';
}
}
campo.addEventListener('blur', verificarExistencia);
Tres decisiones de este código merecen comentario. La variable ultimoConsultado evita repetir la consulta si el usuario entra y sale del campo sin cambiar nada; cada consulta cuesta un crédito. El estado "Verificando..." le dice al usuario que algo está pasando, porque una consulta de red tarda lo suficiente para que un campo que "no hace nada" parezca roto. Y cuando el servidor responde 503, el formulario no bloquea: permite continuar y avisa que se revisará después. Bloquear a un cliente porque tu proveedor de datos tuvo un minuto malo es peor que registrarlo como pendiente.
Si quieres que la consulta ocurra mientras el usuario escribe (en vez de al salir del campo), necesitas un debounce para no disparar una petición por tecla. El patrón completo, con estados de carga y caché, está en autocompletar nombres con el DNI.
En resumen
- Formato (longitud, prefijo, dígito verificador) se valida en el navegador y en el servidor con el mismo módulo.
- Existencia (padrón de SUNAT o RENIEC) solo se valida en el servidor, a través de tu propio endpoint.
- El servidor repite la validación de formato y decide qué se guarda; el frontend es cortesía, no seguridad.
- Muestra errores al salir del campo, con mensajes que digan qué está mal, y nunca inventes "no existe" desde el navegador.
- Para el RUC, revisa
estadoycondicion, no solo que exista. - Si la verificación falla por red o créditos, deja continuar y marca el registro como pendiente.
Preguntas frecuentes
¿Es obligatorio validar en el frontend si ya valido en el backend?
Obligatorio no, pero muy recomendable. Sin validación en el navegador, cada error de tipeo viaja al servidor y, si pasa el formato, gasta una consulta en un número que no existe. Validar en el frontend es gratis y mejora la experiencia; validar en el backend es lo que protege tus datos.
¿Qué pasa si el usuario desactiva JavaScript?
El formulario se envía sin validación previa y el servidor la hace completa al recibirlo. Por eso la validación al guardar tiene que ser independiente del endpoint de consulta y del código del navegador. Si tu backend está bien hecho, un usuario sin JavaScript solo pierde comodidad, no abre un agujero.
¿Debo consultar la existencia cada vez que se edita el cliente?
No. Guarda la fecha de la última verificación y vuelve a consultar solo si pasó un tiempo razonable (días, no minutos) o si el usuario cambió el número. Para el RUC, una revisión periódica del estado y la condición sí tiene sentido, porque cambian; los nombres de una persona casi nunca.
¿Puedo reutilizar el mismo módulo de validación en React, Vue o Angular?
Sí. validadores.js no toca el DOM, así que se importa en cualquier framework y también en Node. Lo único que cambia es cómo conectas el resultado al estado del componente y cuándo disparas la verificación de existencia.






