Validación Zod con Claude Code en TypeScript
Implementa validación Zod para formularios, APIs, variables de entorno, webhooks y pruebas con Claude Code.
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 uso | Entrada | Qué protege Zod |
|---|---|---|
| Formulario | Datos del navegador | Campos vacíos, email, longitud, consentimiento |
| API request/response | request.json() y JSON devuelto | Payload inválido, contrato de respuesta, estados |
| Variables de entorno | process.env | Secretos ausentes, URLs inválidas, puertos |
| Webhook payload | POST de terceros | Tipo de evento, IDs, importes, flujo de firma |
| Antes de DB | Objeto ya transformado | Campos 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.
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.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.