Sistema de Validación
Fox Framework incluye un potente sistema de validación que permite verificar datos de entrada, parámetros de request, cuerpos JSON y formularios de manera elegante y tipada.
Características Principales
- Validación Tipada: Integración completa con TypeScript
- Esquemas Reutilizables: Define una vez, usa en múltiples lugares
- Middleware de Validación: Integración directa con el sistema de rutas
- Mensajes Personalizables: Control total sobre los mensajes de error
- Validación Asíncrona: Soporte para reglas que requieren operaciones asíncronas
- Transformación de Datos: Conversión automática de tipos
- Composición de Validadores: Combina reglas para crear validaciones complejas
Uso Básico
Definición de Esquemas
import { Schema, Validators } from '@foxframework/core';
// Definir un esquema para usuario
const userSchema = new Schema({
name: Validators.string().required().min(2).max(100),
email: Validators.email().required(),
age: Validators.number().min(18).optional(),
role: Validators.enum(['admin', 'user', 'guest']).default('user')
});
// Interfaz generada automáticamente desde el esquema
type User = typeof userSchema.type;
// Equivale a:
// interface User {
// name: string;
// email: string;
// age?: number;
// role: 'admin' | 'user' | 'guest';
// }Validación de Objetos
import { validate } from '@foxframework/core';
// Datos a validar
const data = {
name: 'John Doe',
email: 'john@example.com',
age: 25
};
// Validación sencilla
try {
const validUser = validate(data, userSchema);
console.log('Usuario válido:', validUser);
// validUser es de tipo User y tiene todas las propiedades validadas
} catch (error) {
console.error('Error de validación:', error.message);
}
// Validación asíncrona
async function validateUser(data) {
try {
const validUser = await validate.async(data, userSchema);
return validUser;
} catch (error) {
throw error;
}
}Middleware de Validación
import { Router, validateBody, validateQuery, validateParams } from '@foxframework/core';
const router = new Router();
// Definir esquemas
const createUserSchema = new Schema({
name: Validators.string().required(),
email: Validators.email().required(),
password: Validators.string().min(8).required()
});
const userIdSchema = new Schema({
id: Validators.uuid().required()
});
const userFiltersSchema = new Schema({
role: Validators.enum(['admin', 'user']).optional(),
active: Validators.boolean().optional()
});
// Aplicar validación como middleware
router.post('/users',
validateBody(createUserSchema),
async (req, res) => {
// En este punto req.body es de tipo typeof createUserSchema.type
// y ha sido validado completamente
const { name, email, password } = req.body;
const user = await userService.create({ name, email, password });
res.status(201).json(user);
}
);
router.get('/users/:id',
validateParams(userIdSchema),
async (req, res) => {
// req.params.id es un UUID válido garantizado
const user = await userService.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
res.json(user);
}
);
router.get('/users',
validateQuery(userFiltersSchema),
async (req, res) => {
// req.query contiene filtros validados y convertidos al tipo correcto
const users = await userService.findAll(req.query);
res.json(users);
}
);Validadores Incluidos
Fox Framework incluye validadores para los tipos más comunes:
Tipos Básicos
// String
Validators.string()
.required() // Campo obligatorio
.min(2) // Longitud mínima
.max(100) // Longitud máxima
.matches(/^[a-z]+$/) // Expresión regular
.email() // Debe ser un email válido
.url() // Debe ser una URL válida
.trim() // Elimina espacios al inicio y final
.lowercase() // Convierte a minúsculas
// Number
Validators.number()
.required()
.min(0) // Valor mínimo
.max(100) // Valor máximo
.integer() // Debe ser un entero
.positive() // Debe ser positivo
.negative() // Debe ser negativo
// Boolean
Validators.boolean()
.required()
// Date
Validators.date()
.required()
.min(new Date('2023-01-01')) // Fecha mínima
.max(new Date()) // Fecha máxima
.iso() // Debe ser formato ISO
// Array
Validators.array(Validators.string())
.required()
.min(1) // Mínimo número de elementos
.max(10) // Máximo número de elementos
.unique() // Elementos únicosValidadores Avanzados
// Object
Validators.object({
name: Validators.string().required(),
address: Validators.object({
street: Validators.string().required(),
city: Validators.string().required()
})
})
// Union
Validators.union([
Validators.string(),
Validators.number()
])
// Enum
Validators.enum(['pending', 'completed', 'failed'])
// Custom
Validators.custom((value) => {
if (value !== 'special_value') {
return 'Valor debe ser special_value';
}
})
// Conditional
Validators.object({
type: Validators.enum(['personal', 'business']).required(),
taxId: Validators.string().when('type', {
is: 'business',
then: Validators.required(),
otherwise: Validators.forbidden()
})
})Mensajes Personalizados
Personaliza los mensajes de error para cada validador:
const schema = new Schema({
username: Validators.string()
.required('El nombre de usuario es obligatorio')
.min(3, 'El nombre de usuario debe tener al menos 3 caracteres')
.max(20, 'El nombre de usuario no puede tener más de 20 caracteres'),
email: Validators.string()
.required('El email es obligatorio')
.email('Formato de email inválido')
});
// También puedes configurar mensajes globales
Schema.setErrorMessages({
required: '{field} es un campo obligatorio',
min: '{field} debe tener al menos {min} caracteres',
max: '{field} no puede tener más de {max} caracteres',
email: '{field} debe ser un email válido'
});Transformación de Datos
El sistema permite transformar los datos durante la validación:
const schema = new Schema({
// Convertir a número
age: Validators.number()
.transform((value) => typeof value === 'string' ? parseInt(value, 10) : value),
// Formatear fecha
createdAt: Validators.date()
.transform((value) => new Date(value)),
// Normalizar email
email: Validators.string()
.email()
.transform((value) => value.toLowerCase().trim())
});Validación Asíncrona
Para validaciones que requieren operaciones asíncronas (como verificar un email en la base de datos):
import { Schema, Validators } from '@foxframework/core';
import { userRepository } from './repositories';
const registerSchema = new Schema({
email: Validators.string()
.email()
.required()
.asyncCustom(async (value) => {
const userExists = await userRepository.findByEmail(value);
if (userExists) {
return 'Este email ya está registrado';
}
}),
username: Validators.string()
.required()
.asyncCustom(async (value) => {
const userExists = await userRepository.findByUsername(value);
if (userExists) {
return 'Este nombre de usuario ya está en uso';
}
})
});
// Uso
router.post('/register', async (req, res) => {
try {
const validData = await validate.async(req.body, registerSchema);
// Continuar con el registro
} catch (error) {
res.status(400).json({ errors: error.details });
}
});Validación de Formularios
Para validar formularios HTML con soporte para archivos:
import { Schema, Validators } from '@foxframework/core';
const fileSchema = new Schema({
name: Validators.string().required(),
size: Validators.number().max(10 * 1024 * 1024), // 10MB máximo
mimetype: Validators.string().matches(/^image\/(jpeg|png|gif)$/),
});
const formSchema = new Schema({
title: Validators.string().required(),
description: Validators.string().optional(),
categories: Validators.array(Validators.string()).min(1),
image: Validators.file(fileSchema).required()
});
router.post('/upload',
upload.single('image'), // Middleware de multer
validateForm(formSchema),
(req, res) => {
// req.body y req.file ya están validados
// req.validatedData contiene todos los campos validados incluyendo archivos
const { title, description, categories, image } = req.validatedData;
// ...
}
);Composición de Esquemas
Puedes componer y reutilizar esquemas para estructuras complejas:
// Esquema base para dirección
const addressSchema = new Schema({
street: Validators.string().required(),
city: Validators.string().required(),
state: Validators.string().required(),
postalCode: Validators.string().required(),
country: Validators.string().required()
});
// Esquema para contacto
const contactSchema = new Schema({
email: Validators.email().required(),
phone: Validators.string().optional()
});
// Esquema para cliente que reutiliza los esquemas anteriores
const customerSchema = new Schema({
name: Validators.string().required(),
address: Validators.schema(addressSchema),
billingAddress: Validators.schema(addressSchema).optional(),
contact: Validators.schema(contactSchema)
});
// Para casos de herencia
const employeeSchema = customerSchema.extend({
employeeId: Validators.string().required(),
department: Validators.string().required()
});Integración con Clases
Para proyectos que utilizan clases, se pueden integrar los esquemas con decoradores:
import { validate, schema, field } from '@foxframework/core';
@schema()
class User {
@field(Validators.string().required())
name: string;
@field(Validators.email().required())
email: string;
@field(Validators.number().min(18))
age?: number;
constructor(data?: Partial<User>) {
if (data) {
Object.assign(this, validate(data, User.schema));
}
}
}
// Uso
try {
const user = new User({
name: 'John',
email: 'john@example.com',
age: 30
});
} catch (error) {
console.error('Validación fallida:', error.details);
}Personalización Avanzada
Creando Validadores Personalizados
import { Validator, ValidatorContext } from '@foxframework/core';
// Validador personalizado
class CreditCardValidator extends Validator<string> {
private type: 'visa' | 'mastercard' | 'all' = 'all';
constructor() {
super();
}
visa(): this {
this.type = 'visa';
return this;
}
mastercard(): this {
this.type = 'mastercard';
return this;
}
validate(value: any, context: ValidatorContext): string | undefined {
// Validación básica de tipo
if (typeof value !== 'string') {
return this.createError('creditCard.base', context);
}
// Eliminar espacios y guiones
const sanitized = value.replace(/[\s-]/g, '');
// Debe ser solo dígitos
if (!/^\d+$/.test(sanitized)) {
return this.createError('creditCard.format', context);
}
// Algoritmo de Luhn para validar número de tarjeta
if (!this.luhnCheck(sanitized)) {
return this.createError('creditCard.invalid', context);
}
// Validar tipo específico si se ha solicitado
if (this.type === 'visa' && !sanitized.startsWith('4')) {
return this.createError('creditCard.visa', context);
} else if (this.type === 'mastercard' && !(
sanitized.startsWith('51') ||
sanitized.startsWith('52') ||
sanitized.startsWith('53') ||
sanitized.startsWith('54') ||
sanitized.startsWith('55')
)) {
return this.createError('creditCard.mastercard', context);
}
return undefined; // Validación exitosa
}
private luhnCheck(cardNumber: string): boolean {
let sum = 0;
let shouldDouble = false;
// Recorrer de derecha a izquierda
for (let i = cardNumber.length - 1; i >= 0; i--) {
let digit = parseInt(cardNumber.charAt(i));
if (shouldDouble) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
shouldDouble = !shouldDouble;
}
return sum % 10 === 0;
}
}
// Registrar el validador personalizado
Validators.register('creditCard', () => new CreditCardValidator());
// Usar el validador personalizado
const paymentSchema = new Schema({
cardNumber: Validators.creditCard().visa(),
expiryDate: Validators.string().matches(/^\d{2}\/\d{2}$/),
cvv: Validators.string().length(3)
});Extendiendo el Sistema de Validación
import { Schema, Validators, extend } from '@foxframework/core';
// Agregar métodos personalizados al sistema de validación
extend(Validators, {
// Validador de contraseña fuerte
password() {
return Validators.string()
.min(8)
.matches(/[a-z]/, 'Debe contener al menos una letra minúscula')
.matches(/[A-Z]/, 'Debe contener al menos una letra mayúscula')
.matches(/[0-9]/, 'Debe contener al menos un número')
.matches(/[\W_]/, 'Debe contener al menos un carácter especial');
},
// Validador de slug
slug() {
return Validators.string()
.matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Debe ser un slug válido (minúsculas, números y guiones)');
},
// Validador de código postal español
postalCodeES() {
return Validators.string()
.matches(/^[0-9]{5}$/, 'Debe ser un código postal español válido (5 dígitos)');
}
});
// Uso
const schema = new Schema({
username: Validators.slug().required(),
password: Validators.password().required(),
postalCode: Validators.postalCodeES().optional()
});Desempeño y Optimización
El sistema de validación de Fox Framework está diseñado para ser eficiente incluso con grandes volúmenes de datos:
- Validación Parcial: Valida solo los campos necesarios cuando es apropiado
- Validación Temprana: Falla rápido al encontrar el primer error
- Memorización: Caché de resultados para validaciones repetidas
- Compilación de Esquemas: Los esquemas se compilan para ejecución óptima
// Configuración de optimización
Schema.configure({
abortEarly: true, // Detener en el primer error
cacheResults: true, // Habilitar caché
compile: true, // Compilar esquema para mayor velocidad
stripUnknown: true // Eliminar campos desconocidos
});
// Para validaciones específicas
const validData = validate(inputData, userSchema, {
abortEarly: false, // Recopilar todos los errores
context: { role: 'admin' } // Contexto para validación condicional
});Integración con OpenAPI/Swagger
Fox Framework permite generar especificaciones OpenAPI automáticamente desde los esquemas de validación:
import { generateOpenApi } from '@foxframework/core';
const userSchema = new Schema({
id: Validators.uuid().required(),
name: Validators.string().required().min(2).max(100),
email: Validators.email().required(),
createdAt: Validators.date().required()
});
// Generar esquema OpenAPI
const openApiSchema = generateOpenApi(userSchema);
// Resultado:
// {
// type: 'object',
// required: ['id', 'name', 'email', 'createdAt'],
// properties: {
// id: {
// type: 'string',
// format: 'uuid'
// },
// name: {
// type: 'string',
// minLength: 2,
// maxLength: 100
// },
// email: {
// type: 'string',
// format: 'email'
// },
// createdAt: {
// type: 'string',
// format: 'date-time'
// }
// }
// }Ejemplo Completo
A continuación se muestra un ejemplo completo de una API usando el sistema de validación:
import {
FoxFactory,
Router,
Schema,
Validators,
validateBody,
validateParams,
validateQuery
} from '@foxframework/core';
// Esquemas de validación
const createProductSchema = new Schema({
name: Validators.string().required().min(3).max(100),
description: Validators.string().optional(),
price: Validators.number().required().min(0),
stock: Validators.number().integer().min(0).default(0),
categories: Validators.array(Validators.string()).optional()
});
const productIdSchema = new Schema({
id: Validators.uuid().required()
});
const productFiltersSchema = new Schema({
minPrice: Validators.number().optional(),
maxPrice: Validators.number().optional(),
category: Validators.string().optional(),
sortBy: Validators.enum(['price', 'name', 'stock']).optional().default('name'),
sortDir: Validators.enum(['asc', 'desc']).optional().default('asc'),
page: Validators.number().integer().min(1).optional().default(1),
limit: Validators.number().integer().min(1).max(100).optional().default(20)
});
// Configuración de rutas
const router = new Router();
// Crear producto
router.post('/products',
validateBody(createProductSchema),
async (req, res) => {
try {
const product = await productService.create(req.body);
res.status(201).json(product);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
);
// Obtener producto por ID
router.get('/products/:id',
validateParams(productIdSchema),
async (req, res) => {
try {
const product = await productService.findById(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Producto no encontrado' });
}
res.json(product);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
);
// Listar productos con filtros
router.get('/products',
validateQuery(productFiltersSchema),
async (req, res) => {
try {
const {
minPrice, maxPrice, category,
sortBy, sortDir, page, limit
} = req.query;
const filters = { minPrice, maxPrice, category };
const pagination = { page, limit };
const sorting = { field: sortBy, direction: sortDir };
const result = await productService.findAll(filters, pagination, sorting);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
);
// Actualizar producto
router.put('/products/:id',
validateParams(productIdSchema),
validateBody(createProductSchema),
async (req, res) => {
try {
const product = await productService.update(req.params.id, req.body);
if (!product) {
return res.status(404).json({ error: 'Producto no encontrado' });
}
res.json(product);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
);
// Eliminar producto
router.delete('/products/:id',
validateParams(productIdSchema),
async (req, res) => {
try {
const deleted = await productService.delete(req.params.id);
if (!deleted) {
return res.status(404).json({ error: 'Producto no encontrado' });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
}
);
// Crear servidor
const server = FoxFactory.createServer({
router,
port: 3000
});
server.start();