Api
Validation
Validation system with Zod and React Hook Form
Validation
Yuno Demo uses Zod for schema validation and React Hook Form for form management, providing robust validation and type safety.
Installation
Validation dependencies are already included in the project:
npm install zod react-hook-form @hookform/resolversEsquemas de validación básicos
import { z } from "zod";
export const emailSchema = z
.string()
.email({ message: "Dirección de email inválida" })
.min(1, { message: "El email es requerido" });Contraseña
export const passwordSchema = z
.string()
.min(8, { message: "La contraseña debe tener al menos 8 caracteres" })
.regex(/[a-z]/, { message: "Debe contener al menos una minúscula" })
.regex(/[A-Z]/, { message: "Debe contener al menos una mayúscula" })
.regex(/[0-9]/, { message: "Debe contener al menos un número" })
.regex(/[^a-zA-Z0-9]/, { message: "Debe contener al menos un carácter especial" });Nombre
export const nameSchema = z
.string()
.min(2, { message: "El nombre debe tener al menos 2 caracteres" })
.max(50, { message: "El nombre no puede exceder 50 caracteres" })
.regex(/^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+$/, {
message: "El nombre solo puede contener letras"
});Teléfono
export const phoneSchema = z
.string()
.regex(/^\+?[1-9]\d{1,14}$/, {
message: "Número de teléfono inválido"
});URL
export const urlSchema = z
.string()
.url({ message: "URL inválida" })
.startsWith("https://", { message: "La URL debe usar HTTPS" });Esquemas compuestos
Formulario de registro
import { z } from "zod";
export const registerFormSchema = z
.object({
name: nameSchema,
email: emailSchema,
phone: phoneSchema,
password: passwordSchema,
confirmPassword: z.string(),
termsAccepted: z.boolean().refine((val) => val === true, {
message: "Debes aceptar los términos y condiciones",
}),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Las contraseñas no coinciden",
});
type RegisterFormData = z.infer<typeof registerFormSchema>;Formulario de contacto
export const contactFormSchema = z.object({
name: nameSchema,
email: emailSchema,
subject: z.string().min(5, { message: "El asunto es muy corto" }),
message: z
.string()
.min(10, { message: "El mensaje debe tener al menos 10 caracteres" })
.max(500, { message: "El mensaje no puede exceder 500 caracteres" }),
attachments: z
.array(z.instanceof(File))
.max(3, { message: "Máximo 3 archivos" })
.optional(),
});Uso con React Hook Form
Ejemplo básico
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
const formSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
age: z.number().min(18).max(120),
});
export function MyForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
age: 18,
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Usuario</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>
Tu nombre de usuario público
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="email@ejemplo.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Enviar</Button>
</form>
</Form>
);
}Validación condicional
Campos dependientes
const paymentSchema = z
.object({
paymentMethod: z.enum(["card", "paypal", "crypto"]),
cardNumber: z.string().optional(),
paypalEmail: z.string().optional(),
walletAddress: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.paymentMethod === "card" && !data.cardNumber) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Número de tarjeta requerido",
path: ["cardNumber"],
});
}
if (data.paymentMethod === "paypal" && !data.paypalEmail) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Email de PayPal requerido",
path: ["paypalEmail"],
});
}
if (data.paymentMethod === "crypto" && !data.walletAddress) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Dirección de wallet requerida",
path: ["walletAddress"],
});
}
});Validación de archivos
const fileSchema = z
.instanceof(File)
.refine((file) => file.size <= 5 * 1024 * 1024, {
message: "El archivo no puede superar 5MB",
})
.refine(
(file) => ["image/jpeg", "image/png", "image/webp"].includes(file.type),
{
message: "Solo se permiten imágenes JPG, PNG o WebP",
}
);
const uploadSchema = z.object({
avatar: fileSchema,
documents: z.array(fileSchema).max(3, {
message: "Máximo 3 documentos",
}),
});Validación asíncrona
const usernameSchema = z.string().refine(
async (username) => {
// Verificar si el username está disponible
const response = await fetch(`/api/check-username?username=${username}`);
const { available } = await response.json();
return available;
},
{
message: "Este nombre de usuario ya está en uso",
}
);Validación de arrays
const tagsSchema = z
.array(z.string().min(2).max(20))
.min(1, { message: "Debes agregar al menos una etiqueta" })
.max(5, { message: "Máximo 5 etiquetas" });
const itemsSchema = z
.array(
z.object({
name: z.string(),
quantity: z.number().positive(),
price: z.number().positive(),
})
)
.nonempty({ message: "Debes agregar al menos un item" });Validación de objetos anidados
const addressSchema = z.object({
street: z.string().min(5),
city: z.string().min(2),
state: z.string().length(2),
zipCode: z.string().regex(/^\d{5}$/),
country: z.string().length(2),
});
const userSchema = z.object({
personalInfo: z.object({
firstName: z.string(),
lastName: z.string(),
birthDate: z.date(),
}),
shippingAddress: addressSchema,
billingAddress: addressSchema.optional(),
sameAsShipping: z.boolean(),
});Transformaciones
const trimmedString = z.string().trim();
const normalizedEmail = z
.string()
.email()
.transform((val) => val.toLowerCase());
const stringToNumber = z
.string()
.transform((val) => Number.parseInt(val, 10))
.pipe(z.number().positive());
const dateString = z
.string()
.transform((str) => new Date(str))
.pipe(z.date());Mensajes de error personalizados
const customErrorSchema = z.object({
email: z.string().email({
message: "Por favor ingresa un email válido como ejemplo@dominio.com",
}),
password: z
.string()
.min(8, {
message: "La contraseña es demasiado corta. Usa al menos 8 caracteres.",
})
.max(100, {
message: "La contraseña es demasiado larga. Usa máximo 100 caracteres.",
}),
});Validación en el servidor
// app/api/register/route.ts
import { NextResponse } from "next/server";
import { registerFormSchema } from "@/lib/validation-schemas";
export async function POST(request: Request) {
try {
const body = await request.json();
// Validar datos
const validatedData = registerFormSchema.parse(body);
// Procesar datos validados
// ...
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Error interno del servidor" },
{ status: 500 }
);
}
}Utilidades de validación
// lib/validation-utils.ts
// Validar email con dominio específico
export const createEmailWithDomainSchema = (domain: string) => {
return z.string().email().refine(
(email) => email.endsWith(`@${domain}`),
{ message: `El email debe ser del dominio @${domain}` }
);
};
// Validar rango de fechas
export const dateRangeSchema = z
.object({
startDate: z.date(),
endDate: z.date(),
})
.refine((data) => data.endDate > data.startDate, {
message: "La fecha final debe ser posterior a la fecha inicial",
path: ["endDate"],
});
// Validar al menos un campo requerido
export const atLeastOneRequired = <T extends z.ZodRawShape>(schema: T) => {
return z.object(schema).refine(
(data) => Object.values(data).some((value) => value !== undefined),
{ message: "Al menos un campo es requerido" }
);
};Testing de validación
import { describe, it, expect } from "vitest";
import { registerFormSchema } from "@/lib/validation-schemas";
describe("registerFormSchema", () => {
it("should validate correct data", () => {
const validData = {
name: "John Doe",
email: "john@example.com",
phone: "+1234567890",
password: "SecureP@ss123",
confirmPassword: "SecureP@ss123",
termsAccepted: true,
};
expect(() => registerFormSchema.parse(validData)).not.toThrow();
});
it("should reject invalid email", () => {
const invalidData = {
email: "invalid-email",
// ... otros campos
};
expect(() => registerFormSchema.parse(invalidData)).toThrow();
});
});Siguiente paso
Aprende sobre Temas y Personalización para estilizar tu aplicación.