API Validación DNI: Implementación en JavaScript y Python

Aprende a implementar APIs de validación de DNI español en JavaScript y Python. Código completo, mejores prácticas y consideraciones de seguridad.

Introducción a las APIs de Validación DNI

La validación de DNI es un proceso crítico en muchas aplicaciones web y móviles. Implementar una API robusta de validación no solo mejora la experiencia del usuario, sino que también garantiza la integridad de los datos y el cumplimiento de regulaciones.

Algoritmo de Validación DNI

Fundamentos Matemáticos

El DNI español utiliza un algoritmo de módulo 23 para calcular la letra de control:

const LETRAS_DNI = 'TRWAGMYFPDXBNJZSQVHLCKE';

function calcularLetraDNI(numero) {
    const resto = numero % 23;
    return LETRAS_DNI[resto];
}

Validación Completa

Un DNI válido debe cumplir:

  • 8 dígitos numéricos
  • 1 letra de control calculada correctamente
  • Formato correcto (NNNNNNNNL)

Implementación en JavaScript

API REST con Express.js

const express = require('express');
const app = express();

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Función de validación
function validarDNI(dni) {
    const dniRegex = /^[0-9]{8}[TRWAGMYFPDXBNJZSQVHLCKE]$/i;
    
    if (!dniRegex.test(dni)) {
        return {
            valido: false,
            error: 'Formato inválido',
            codigo: 'FORMATO_INVALIDO'
        };
    }
    
    const numero = parseInt(dni.slice(0, 8));
    const letra = dni.slice(8).toUpperCase();
    const letraCalculada = calcularLetraDNI(numero);
    
    return {
        valido: letra === letraCalculada,
        numero: numero,
        letra: letra,
        letraCalculada: letraCalculada,
        error: letra !== letraCalculada ? 'Letra de control incorrecta' : null
    };
}

// Endpoint de validación
app.post('/api/validar-dni', (req, res) => {
    const { dni } = req.body;
    
    if (!dni) {
        return res.status(400).json({
            error: 'DNI requerido',
            codigo: 'DNI_REQUERIDO'
        });
    }
    
    const resultado = validarDNI(dni);
    
    res.json({
        dni: dni,
        timestamp: new Date().toISOString(),
        resultado: resultado
    });
});

// Endpoint GET para validación simple
app.get('/api/validar-dni/:dni', (req, res) => {
    const { dni } = req.params;
    const resultado = validarDNI(dni);
    
    res.json({
        dni: dni,
        valido: resultado.valido,
        timestamp: new Date().toISOString()
    });
});

app.listen(3000, () => {
    console.log('API de validación DNI ejecutándose en puerto 3000');
});

Validación Frontend

class DNIValidator {
    constructor() {
        this.letras = 'TRWAGMYFPDXBNJZSQVHLCKE';
        this.regex = /^[0-9]{8}[TRWAGMYFPDXBNJZSQVHLCKE]$/i;
    }
    
    validar(dni) {
        // Limpiar espacios y normalizar
        dni = dni.replace(/\s/g, '').toUpperCase();
        
        if (!this.regex.test(dni)) {
            return {
                valido: false,
                error: 'Formato inválido'
            };
        }
        
        const numero = parseInt(dni.slice(0, 8));
        const letra = dni.slice(8);
        const letraCalculada = this.letras[numero % 23];
        
        return {
            valido: letra === letraCalculada,
            numero: numero,
            letra: letra,
            letraCalculada: letraCalculada
        };
    }
    
    async validarRemoto(dni) {
        try {
            const response = await fetch('/api/validar-dni', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ dni })
            });
            
            return await response.json();
        } catch (error) {
            return {
                error: 'Error de conexión',
                codigo: 'CONEXION_ERROR'
            };
        }
    }
}

Implementación en Python

API con Flask

from flask import Flask, request, jsonify
from datetime import datetime
import re

app = Flask(__name__)

class DNIValidator:
    LETRAS = 'TRWAGMYFPDXBNJZSQVHLCKE'
    PATRON = re.compile(r'^[0-9]{8}[TRWAGMYFPDXBNJZSQVHLCKE]$', re.IGNORECASE)
    
    @classmethod
    def calcular_letra(cls, numero):
        """Calcula la letra de control para un número de DNI"""
        return cls.LETRAS[numero % 23]
    
    @classmethod
    def validar(cls, dni):
        """Valida un DNI completo"""
        # Limpiar y normalizar
        dni = dni.replace(' ', '').upper()
        
        if not cls.PATRON.match(dni):
            return {
                'valido': False,
                'error': 'Formato inválido',
                'codigo': 'FORMATO_INVALIDO'
            }
        
        numero = int(dni[:8])
        letra = dni[8]
        letra_calculada = cls.calcular_letra(numero)
        
        return {
            'valido': letra == letra_calculada,
            'numero': numero,
            'letra': letra,
            'letra_calculada': letra_calculada,
            'error': None if letra == letra_calculada else 'Letra de control incorrecta'
        }

@app.route('/api/validar-dni', methods=['POST'])
def validar_dni_post():
    data = request.get_json()
    
    if not data or 'dni' not in data:
        return jsonify({
            'error': 'DNI requerido',
            'codigo': 'DNI_REQUERIDO'
        }), 400
    
    dni = data['dni']
    resultado = DNIValidator.validar(dni)
    
    return jsonify({
        'dni': dni,
        'timestamp': datetime.now().isoformat(),
        'resultado': resultado
    })

@app.route('/api/validar-dni/<dni>', methods=['GET'])
def validar_dni_get(dni):
    resultado = DNIValidator.validar(dni)
    
    return jsonify({
        'dni': dni,
        'valido': resultado['valido'],
        'timestamp': datetime.now().isoformat()
    })

@app.route('/api/generar-dni', methods=['GET'])
def generar_dni():
    """Genera un DNI válido aleatorio para testing"""
    import random
    
    numero = random.randint(10000000, 99999999)
    letra = DNIValidator.calcular_letra(numero)
    dni_generado = f"{numero}{letra}"
    
    return jsonify({
        'dni': dni_generado,
        'numero': numero,
        'letra': letra,
        'timestamp': datetime.now().isoformat(),
        'advertencia': 'Solo para testing - No es un DNI real'
    })

if __name__ == '__main__':
    app.run(debug=True)

Validación con FastAPI

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, validator
from datetime import datetime
from typing import Optional

app = FastAPI(title="API Validación DNI", version="1.0.0")

class DNIRequest(BaseModel):
    dni: str
    
    @validator('dni')
    def validar_formato_basico(cls, v):
        if not v or len(v.strip()) == 0:
            raise ValueError('DNI no puede estar vacío')
        return v.strip().upper()

class DNIResponse(BaseModel):
    dni: str
    valido: bool
    numero: Optional[int] = None
    letra: Optional[str] = None
    letra_calculada: Optional[str] = None
    error: Optional[str] = None
    timestamp: datetime

@app.post("/validar-dni", response_model=DNIResponse)
async def validar_dni(request: DNIRequest):
    resultado = DNIValidator.validar(request.dni)
    
    return DNIResponse(
        dni=request.dni,
        valido=resultado['valido'],
        numero=resultado.get('numero'),
        letra=resultado.get('letra'),
        letra_calculada=resultado.get('letra_calculada'),
        error=resultado.get('error'),
        timestamp=datetime.now()
    )

@app.get("/healthcheck")
async def healthcheck():
    return {"status": "ok", "timestamp": datetime.now()}

Consideraciones de Seguridad

Rate Limiting

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutos
    max: 100, // límite de 100 requests por IP
    message: 'Demasiadas solicitudes desde esta IP'
});

app.use('/api/', limiter);

Validación de Entrada

  • Sanitización de datos de entrada
  • Validación de longitud y formato
  • Protección contra inyección de código
  • Logging de intentos sospechosos

CORS y Headers de Seguridad

const cors = require('cors');
const helmet = require('helmet');

app.use(helmet());
app.use(cors({
    origin: ['https://tudominio.com'],
    methods: ['GET', 'POST'],
    credentials: true
}));

Testing y Documentación

Tests Unitarios (Jest)

const { validarDNI } = require('./dni-validator');

describe('Validación DNI', () => {
    test('DNI válido', () => {
        const resultado = validarDNI('12345678Z');
        expect(resultado.valido).toBe(true);
    });
    
    test('DNI formato inválido', () => {
        const resultado = validarDNI('1234567');
        expect(resultado.valido).toBe(false);
    });
    
    test('Letra incorrecta', () => {
        const resultado = validarDNI('12345678A');
        expect(resultado.valido).toBe(false);
    });
});

Documentación con Swagger

const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const options = {
    definition: {
        openapi: '3.0.0',
        info: {
            title: 'API Validación DNI',
            version: '1.0.0',
            description: 'API para validar DNI españoles'
        }
    },
    apis: ['./routes/*.js']
};

const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

Despliegue y Monitorización

Docker

FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Monitorización

  • Logs estructurados
  • Métricas de rendimiento
  • Alertas de errores
  • Dashboard de uso

Mejores Prácticas

Rendimiento

  • Cache de resultados frecuentes
  • Validación asíncrona
  • Pooling de conexiones
  • Compresión de respuestas

Escalabilidad

  • Arquitectura sin estado
  • Load balancing
  • Auto-scaling
  • CDN para recursos estáticos

La implementación de una API de validación DNI robusta requiere atención tanto a la precisión del algoritmo como a la seguridad y el rendimiento del sistema en producción.