Use Cases (Actualizado: 2/6/2026)

Validación Zod con Claude Code en TypeScript

Implementa validación Zod para formularios, APIs, variables de entorno, webhooks y pruebas con Claude Code.

Validación Zod con Claude Code en TypeScript

Por qué Zod debe vivir en los límites de entrada

TypeScript ayuda mientras escribes código, pero no valida automáticamente los datos que llegan en tiempo de ejecución. Un formulario del navegador, un body de API, un Webhook payload, process.env o un objeto justo antes de guardarse en la base de datos pueden tener una forma distinta a la esperada. La validación en runtime consiste en comprobar esos datos antes de que la lógica de negocio los use. Zod permite definir un schema, es decir, una regla explícita para la forma del dato, y derivar de ahí el tipo TypeScript.

Claude Code funciona muy bien con Zod porque la validación es una tarea estructurada: campos, tipos, límites, mensajes de error, casos de uso y pruebas. Si solo pides “haz validación”, el resultado será genérico. Si pides “validación de formulario, API request/response, variables de entorno, Webhook payload, validación antes de insertar en DB y tests”, Claude Code tiene un mapa claro. Para comprobar detalles, usa la documentación oficial de Zod y la documentación de Next.js Route Handlers.

unknown input
  -> Zod schema
  -> safeParse
  -> typed data
  -> business logic
  -> response schema
  -> client

La regla principal es tratar la entrada externa como unknown. No la conviertas con as MyType antes de validarla. Primero Zod, después el tipo inferido.

Casos de uso que debes especificar

Cada frontera necesita una estrategia distinta. Un formulario necesita mensajes útiles para el usuario. Una API necesita responder 400 con errores de campo. Las variables de entorno deben fallar al arrancar. Un webhook debe verificar firma antes de mirar el payload.

Caso de usoEntradaQué protege Zod
FormularioDatos del navegadorCampos vacíos, email, longitud, consentimiento
API request/responserequest.json() y JSON devueltoPayload inválido, contrato de respuesta, estados
Variables de entornoprocess.envSecretos ausentes, URLs inválidas, puertos
Webhook payloadPOST de tercerosTipo de evento, IDs, importes, flujo de firma
Antes de DBObjeto ya transformadoCampos persistibles, enums, IDs obligatorios

Entrega esta tabla a Claude Code. Evita pedir un único schema para todo. El formulario puede tener campos de confirmación o checkboxes; el schema de inserción en DB no debería depender de detalles de UI. Reutiliza piezas pequeñas como emailSchema o idSchema, no objetos completos sin revisar. Para formularios, consulta la guía interna de React Hook Form. Para APIs más amplias, revisa también desarrollo con tRPC.

Crear el schema base

Este ejemplo valida un formulario de contacto. Es suficientemente pequeño para copiarlo y adaptarlo. Al pedirlo a Claude Code, indica idioma de mensajes, campos obligatorios, límites de longitud y que no agregue campos de base de datos como id o createdAt.

// src/lib/schemas/contact.ts
import { z } from "zod";

export const contactFormSchema = z.object({
  name: z
    .string()
    .trim()
    .min(1, "Introduce tu nombre")
    .max(80, "El nombre debe tener 80 caracteres o menos"),
  email: z
    .string()
    .trim()
    .email("Introduce un email válido"),
  plan: z.enum(["trial", "team", "enterprise"]),
  message: z
    .string()
    .trim()
    .min(10, "El mensaje debe tener al menos 10 caracteres")
    .max(2000, "El mensaje debe tener 2000 caracteres o menos"),
  agreedToPolicy: z
    .boolean()
    .refine((value) => value, "Debes aceptar la política de privacidad"),
});

export type ContactFormInput = z.infer<typeof contactFormSchema>;

trim() evita que un campo con solo espacios sea válido. z.enum limita una cadena a opciones conocidas. z.infer evita mantener un tipo separado a mano, una fuente común de divergencias.

Convertir safeParse en errores HTTP

parse lanza una excepción cuando falla. Es útil para configuración de arranque. safeParse devuelve un resultado controlado, mejor para formularios y APIs donde quieres responder con 400.

// src/lib/validation.ts
import { z } from "zod";

export type ValidationProblem = {
  path: string;
  message: string;
};

export function validateInput<TSchema extends z.ZodTypeAny>(
  schema: TSchema,
  input: unknown,
):
  | { ok: true; data: z.infer<TSchema> }
  | { ok: false; status: 400; errors: ValidationProblem[] } {
  const result = schema.safeParse(input);

  if (!result.success) {
    return {
      ok: false,
      status: 400,
      errors: result.error.issues.map((issue) => ({
        path: issue.path.join(".") || "_root",
        message: issue.message,
      })),
    };
  }

  return { ok: true, data: result.data };
}

Este helper crea una política común. En una app multilingüe, puedes devolver claves de mensaje en lugar de textos finales y traducirlas en la UI.

Validar request y response en Next.js

La API debe validar la entrada antes de la lógica de negocio y también puede validar la salida. El schema de respuesta detecta cambios accidentales en el contrato público.

// app/api/contact/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import {
  contactFormSchema,
  type ContactFormInput,
} from "@/lib/schemas/contact";
import { validateInput } from "@/lib/validation";

const contactResponseSchema = z.object({
  id: z.string().min(1),
  status: z.enum(["queued"]),
});

async function saveContact(input: ContactFormInput) {
  // Replace this with your database insert.
  return {
    id: `contact_${Date.now()}`,
    status: "queued" as const,
    email: input.email,
  };
}

export async function POST(request: Request) {
  const body: unknown = await request.json();
  const validated = validateInput(contactFormSchema, body);

  if (!validated.ok) {
    return NextResponse.json(
      { message: "Revisa los datos enviados", errors: validated.errors },
      { status: validated.status },
    );
  }

  const saved = await saveContact(validated.data);
  const response = contactResponseSchema.parse(saved);

  return NextResponse.json(response, { status: 201 });
}

Para webhooks, la secuencia correcta es firma primero, schema después, lógica al final. Pide a Claude Code que separe verifySignature, webhookPayloadSchema y handleWebhookEvent. Así la revisión de seguridad es mucho más clara.

Validar variables de entorno al arrancar

Las variables de entorno son cadenas o undefined. Si falta DATABASE_URL, la aplicación debe fallar al iniciar, no en la primera petición real.

// src/env.ts
import { z } from "zod";

const envSchema = z.object({
  NODE_ENV: z
    .enum(["development", "test", "production"])
    .default("development"),
  DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"),
  NEXT_PUBLIC_APP_URL: z.string().url("NEXT_PUBLIC_APP_URL must be a valid URL"),
  WEBHOOK_SECRET: z.string().min(32, "WEBHOOK_SECRET must be at least 32 chars"),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  console.error(
    "Invalid environment variables",
    parsed.error.flatten().fieldErrors,
  );
  throw new Error("Invalid environment variables");
}

export const env = parsed.data;

z.coerce.number() es correcto aquí porque el origen es texto. No lo apliques sin pensar a cualquier JSON body. Decide antes cómo tratar cadenas vacías, espacios y números escritos como texto.

Integración con react-hook-form

La validación del cliente mejora la experiencia, pero no reemplaza la validación del servidor. El navegador puede saltarse; el Route Handler no.

// src/components/contact-form.tsx
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
  contactFormSchema,
  type ContactFormInput,
} from "@/lib/schemas/contact";

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<ContactFormInput>({
    resolver: zodResolver(contactFormSchema),
    defaultValues: {
      name: "",
      email: "",
      plan: "trial",
      message: "",
      agreedToPolicy: false,
    },
  });

  async function onSubmit(values: ContactFormInput) {
    const response = await fetch("/api/contact", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(values),
    });

    if (!response.ok) {
      throw new Error("Failed to send contact request");
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} aria-invalid={Boolean(errors.name)} />
      {errors.name && <p>{errors.name.message}</p>}

      <input {...register("email")} aria-invalid={Boolean(errors.email)} />
      {errors.email && <p>{errors.email.message}</p>}

      <select {...register("plan")}>
        <option value="trial">Trial</option>
        <option value="team">Team</option>
        <option value="enterprise">Enterprise</option>
      </select>

      <textarea {...register("message")} />
      {errors.message && <p>{errors.message.message}</p>}

      <label>
        <input type="checkbox" {...register("agreedToPolicy")} />
        I agree to the privacy policy
      </label>
      {errors.agreedToPolicy && <p>{errors.agreedToPolicy.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        Send
      </button>
    </form>
  );
}

Al pedir cambios a Claude Code, añade una regla explícita: no eliminar la validación del servidor. Es una regresión frecuente cuando se trabaja solo sobre la UI.

Prompt de revisión para Claude Code

Después de implementar, usa Claude Code como revisor especializado. Este prompt mantiene el foco.

Review only the Zod validation design in these files.

Check:
1. Every external input is treated as unknown until safeParse or parse succeeds.
2. Route Handlers return 400 with field-level errors for invalid requests.
3. Environment variables fail fast at startup.
4. coerce and transform are used only where the input source is clear.
5. Form schemas, API schemas, and database insert schemas are not over-shared.
6. Error messages are user-facing and ready for localization.

Do not refactor unrelated business logic.
Return findings with file paths, risk level, and a minimal patch suggestion.

Este prompt cubre los fallos principales: confiar solo en tipos TypeScript, mezclar parse y safeParse, abusar de coerce, meter efectos secundarios en transform, olvidar localización y reutilizar schemas demasiado grandes.

Tests para fijar el contrato

Un schema es un contrato de producto. Escribe un caso válido y varios inválidos para que futuras ediciones no relajen las reglas sin querer.

// src/lib/schemas/contact.test.ts
import { describe, expect, it } from "vitest";
import { contactFormSchema } from "./contact";

describe("contactFormSchema", () => {
  it("accepts a valid contact request", () => {
    const result = contactFormSchema.safeParse({
      name: "Masa",
      email: "masa@example.com",
      plan: "team",
      message: "I want to introduce Claude Code to my team.",
      agreedToPolicy: true,
    });

    expect(result.success).toBe(true);
  });

  it("rejects invalid email and short message", () => {
    const result = contactFormSchema.safeParse({
      name: "Masa",
      email: "not-an-email",
      plan: "team",
      message: "short",
      agreedToPolicy: true,
    });

    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.error.issues.map((issue) => issue.path.join("."))).toEqual(
        expect.arrayContaining(["email", "message"]),
      );
    }
  });
});

Para la validación antes de DB, prueba el objeto final que vas a persistir. Así detectas cambios de nombres de campos, enums o transformaciones.

Errores comunes

Primero, los tipos TypeScript no validan en runtime. request.json() as ContactFormInput solo oculta el riesgo al compilador.

Segundo, separa parse y safeParse. Formularios y APIs suelen necesitar safeParse y 400. Configuración y variables de entorno pueden detener el proceso.

Tercero, no abuses de coerce. Es útil para query strings y entorno, pero peligroso si convierte datos sucios en datos aceptados.

Cuarto, no metas efectos secundarios en transform. Escribir en DB, enviar emails o registrar eventos debe ocurrir después de validar.

Quinto, planifica mensajes y localización. En un producto multilingüe, las claves de mensaje suelen ser más sostenibles que textos fijos en cada schema.

Sexto, no reutilices schemas por comodidad. Formulario, API, Webhook y DB pueden parecer similares, pero no son el mismo contrato.

Consultoría y nota de verificación

Si tus validaciones ya están dispersas en formularios, webhooks y rutas API, Claude Code Lab puede ayudarte a definir capas de schema, prompts de revisión y pruebas repetibles. Para acompañamiento, usa la página en inglés de consultoría y formación.

Los ejemplos se revisaron el 2026-06-02 contra la documentación oficial de Zod y Next.js Route Handlers. Asumen un proyecto TypeScript con zod, react-hook-form, @hookform/resolvers y vitest. En proyectos reales, Masa añade autenticación, CSRF o verificación de firma, restricciones de base de datos y pruebas fallidas para cada frontera externa.

#Claude Code #Zod #validation #TypeScript #type safety
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.