Crea schemas de validacion Zod para backend (inline en controllers) y frontend (archivos separados con React Hook Form). Incluye patrones de validacion para documentos espanoles (DNI, NIE, CIF, NSS).
Crea schemas de validacion Zod para backend y frontend.
import { z } from "zod";
const createEmployeeSchema = z.object({
nombre: z.string().min(1, "Nombre requerido"),
primer_apellido: z.string().min(1, "Primer apellido requerido"),
segundo_apellido: z.string().optional(),
email: z.string().email("Email invalido"),
telefono: z.string().regex(/^[0-9]{9}$/, "Telefono debe tener 9 digitos").optional(),
tipo_documento: z.enum(["DNI", "NIE", "Pasaporte", "Tarjeta de residencia"]),
numero_documento: z.string().min(1, "Documento requerido"),
fecha_nacimiento: z.string().refine(val => !isNaN(Date.parse(val)), "Fecha invalida"),
sexo: z.enum(["H", "M"]),
companyId: z.number().int().positive().or(z.string().transform(Number)),
});
// Uso en controller:
export const createEmployee = async (req, res) => {
try {
const validated = createEmployeeSchema.parse(req.body);
// ... logica
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
error: "Datos invalidos",
details: error.errors,
});
}
// ...
}
};
src/schemas/{modulo}Schema.ts)import { z } from "zod";
export const employeeSchema = z.object({
nombre: z.string().min(2, "Minimo 2 caracteres"),
primer_apellido: z.string().min(2, "Minimo 2 caracteres"),
segundo_apellido: z.string().optional(),
tipo_documento: z.enum(["DNI", "NIE", "Pasaporte", "Tarjeta de residencia"]),
numero_documento: z.string().min(1, "Documento requerido"),
email: z.string().email("Email invalido"),
telefono: z.string().regex(/^[0-9]{9}$/, "9 digitos").optional().or(z.literal("")),
fecha_nacimiento: z.string()
.refine(val => /^\d{2}\/\d{2}\/\d{4}$/.test(val), "Formato DD/MM/YYYY"),
}).superRefine((data, ctx) => {
// Validaciones cruzadas por tipo de documento
if (data.tipo_documento === "DNI" && !/^[0-9]{8}[A-Z]$/.test(data.numero_documento)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "DNI invalido (8 digitos + letra)",
path: ["numero_documento"],
});
}
if (data.tipo_documento === "NIE" && !/^[XYZ][0-9]{7}[A-Z]$/.test(data.numero_documento)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "NIE invalido (X/Y/Z + 7 digitos + letra)",
path: ["numero_documento"],
});
}
});
// Tipo inferido para TypeScript
export type EmployeeFormData = z.infer<typeof employeeSchema>;
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { employeeSchema, type EmployeeFormData } from "../../schemas/employeeSchema";
const { register, handleSubmit, formState: { errors } } = useForm<EmployeeFormData>({
resolver: zodResolver(employeeSchema),
});
| Campo | Regex | Descripcion |
|---|---|---|
| DNI | /^[0-9]{8}[A-Z]$/ | 8 digitos + 1 letra mayuscula |
| NIE | /^[XYZ][0-9]{7}[A-Z]$/ | X/Y/Z + 7 digitos + 1 letra |
| CIF | /^[A-Z][0-9]{7}[A-Z0-9]$/ | Letra + 7 digitos + letra/digito |
| CCC | /^[0-9]{11}$/ | 11 digitos (Codigo Cuenta Cotizacion) |
| NSS | /^[0-9]{12}$/ | 12 digitos (Numero Seguridad Social) |
| Telefono | /^[0-9]{9}$/ | 9 digitos |
| Cod. Postal | /^[0-9]{5}$/ | 5 digitos |
| Fecha | DD/MM/YYYY o ISO 8601 | Formato espanol o estandar |
const validarLetraDNI = (dni: string): boolean => {
const letras = "TRWAGMYFPDXBNJZSQVHLCKE";
const numero = parseInt(dni.slice(0, 8));
const letra = dni.charAt(8);
return letras[numero % 23] === letra;
};
// En schema:
numero_documento: z.string()
.regex(/^[0-9]{8}[A-Z]$/, "Formato DNI invalido")
.refine(validarLetraDNI, "Letra de control DNI incorrecta"),
// Permitir string vacio o valor valido