zod-schema

SKILL.md

Zod Schema Skill

Crea schemas de validacion Zod para backend y frontend.

When to Activate

  • Validar input de un endpoint backend
  • Crear schema de validacion para un formulario frontend
  • Validar documentos espanoles (DNI, NIE, CIF, CCC, NSS)
  • Agregar validaciones cruzadas entre campos
  • Conectar validacion Zod con React Hook Form

Backend — Schema Inline en Controller

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,
      });
    }
    // ...
  }
};

Frontend — Schema en Archivo Separado

Schema (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>;

Integracion con React Hook Form

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),
});

Patrones de Validacion para Documentos Espanoles

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

Validaciones Avanzadas

Letra de Control DNI

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"),

Campo Opcional que Puede ser String Vacio

// Permitir string vacio o valor valido
telefono: z.string()
  .regex(/^[0-9]{9}$/, "9 digitos")
  .optional()
  .or(z.literal("")),

Transformar String a Number

// Aceptar string o number, siempre devolver number
companyId: z.number().int().positive()
  .or(z.string().transform(Number)),

Convenciones

Concepto Convencion
Backend Schemas inline en el controller
Frontend Archivos separados en src/schemas/{modulo}Schema.ts
Errores Mensajes en espanol
Opcionales .optional() o .nullable()
Transformaciones .transform() para coercion de tipos
Validacion cruzada .superRefine() para validaciones entre campos
Tipos z.infer<typeof schema> para TypeScript

Checklist

  • Schema definido con todos los campos requeridos
  • Mensajes de error en espanol
  • Validaciones regex para documentos espanoles
  • Validaciones cruzadas con .superRefine() si aplica
  • Tipo inferido exportado (type FormData = z.infer<typeof schema>)
  • Integrado con React Hook Form via zodResolver (frontend)
  • Manejo de z.ZodError en catch (backend)
Weekly Installs
1
First Seen
6 days ago
Installed on
amp1
cline1
trae1
trae-cn1
opencode1
cursor1