Validação Zod com Claude Code em TypeScript
Use Claude Code e Zod para validar formulários, APIs, variáveis de ambiente, webhooks e testes.
Por que validar em runtime com Zod
TypeScript ajuda durante o desenvolvimento, mas não confere automaticamente dados que chegam em runtime. Um formulário do navegador, um body de API, um Webhook payload, process.env ou um objeto antes de entrar no banco pode estar fora do formato esperado. Validação em runtime é checar esse dado antes de entregá-lo à regra de negócio. Zod permite declarar um schema, ou seja, um contrato de dados, e inferir o tipo TypeScript a partir dele.
Claude Code é útil nesse fluxo porque validação é uma tarefa estruturada: campos, tipos, limites, mensagens, pontos de entrada e testes. Um pedido genérico gera código genérico. Um pedido como “formulário, API request/response, variáveis de ambiente, Webhook payload, validação antes do DB e testes” dá contexto suficiente para uma implementação real. Para detalhes, consulte a documentação oficial do Zod e a documentação de Next.js Route Handlers.
unknown input
-> Zod schema
-> safeParse
-> typed data
-> business logic
-> response schema
-> client
A regra prática é simples: entrada externa começa como unknown. Só depois de passar pelo Zod ela vira dado tipado.
Casos de uso para informar ao Claude Code
Cada fronteira precisa de regras próprias. Formulário precisa de mensagem clara para o usuário. API precisa responder 400. Variáveis de ambiente precisam falhar no startup. Webhook precisa verificar assinatura antes de processar o payload.
| Caso de uso | Entrada | O que o Zod protege |
|---|---|---|
| Formulário | Dados do navegador | Campos vazios, email, tamanho, consentimento |
| API request/response | request.json() e JSON de resposta | Payload inválido, contrato de resposta, status |
| Variáveis de ambiente | process.env | Secrets ausentes, URLs, portas |
| Webhook payload | POST de terceiros | Tipo de evento, IDs, valores, assinatura |
| Antes do DB | Objeto já transformado | Campos persistíveis, enums, IDs obrigatórios |
Passe essa tabela para o Claude Code. Não force um schema único para tudo. O formulário pode ter campos de confirmação e checkbox; o schema de insert no banco deve ter outro contrato. Reaproveite peças pequenas, como emailSchema ou idSchema, mas não compartilhe objetos inteiros sem revisar. Para formulários, leia o guia interno de React Hook Form. Para APIs, veja também desenvolvimento com tRPC.
Criar o schema base
O exemplo abaixo valida um formulário de contato. Ele pode ser copiado e ajustado. Ao pedir para Claude Code gerar esse arquivo, especifique idioma das mensagens, obrigatoriedade, limites e que campos de banco como id e createdAt não devem aparecer no schema do formulário.
// src/lib/schemas/contact.ts
import { z } from "zod";
export const contactFormSchema = z.object({
name: z
.string()
.trim()
.min(1, "Informe seu nome")
.max(80, "O nome deve ter no máximo 80 caracteres"),
email: z
.string()
.trim()
.email("Informe um email válido"),
plan: z.enum(["trial", "team", "enterprise"]),
message: z
.string()
.trim()
.min(10, "A mensagem deve ter pelo menos 10 caracteres")
.max(2000, "A mensagem deve ter no máximo 2000 caracteres"),
agreedToPolicy: z
.boolean()
.refine((value) => value, "É necessário aceitar a política de privacidade"),
});
export type ContactFormInput = z.infer<typeof contactFormSchema>;
trim() impede que espaços em branco passem como texto válido. z.enum restringe a string a valores conhecidos. z.infer evita manter uma interface separada e desatualizada.
Transformar safeParse em erro HTTP
parse lança exceção quando falha. Ele serve para configuração de startup. safeParse retorna um resultado controlado, melhor para formulários e APIs que precisam devolver 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 };
}
Esse helper padroniza o formato de erro. Em produto multilíngue, ele pode devolver chaves de mensagem para a interface traduzir.
Validar request e response no Next.js
Uma Route Handler deve validar a entrada antes da regra de negócio. Também vale validar a resposta, porque isso detecta mudanças acidentais no 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: "Revise os dados enviados", errors: validated.errors },
{ status: validated.status },
);
}
const saved = await saveContact(validated.data);
const response = contactResponseSchema.parse(saved);
return NextResponse.json(response, { status: 201 });
}
Em webhooks, a ordem é assinatura, payload schema e só então lógica de negócio. Peça ao Claude Code para separar verifySignature, webhookPayloadSchema e handleWebhookEvent. Isso deixa o review mais objetivo.
Validar variáveis de ambiente no startup
Variáveis de ambiente são strings ou undefined. Se DATABASE_URL não existe, a aplicação deve falhar ao iniciar, não durante a primeira requisição.
// 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() faz sentido para PORT, porque a origem é string. Não use coerção em todo JSON body por padrão. Ela pode aceitar valores sujos sem que ninguém perceba.
Integrar com react-hook-form
Validação no cliente melhora a experiência, mas não substitui a validação no servidor. O navegador pode ser contornado.
// 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>
);
}
Ao pedir alterações para Claude Code, diga explicitamente que a validação do servidor não pode ser removida. É comum melhorar a UI e enfraquecer a API sem perceber.
Prompt de revisão para Claude Code
Depois da implementação, use Claude Code como revisor focado. Um prompt estreito gera feedback acionável.
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.
Esse prompt cobre os riscos principais: confiar só em TypeScript, confundir parse e safeParse, abusar de coerce, colocar efeitos colaterais em transform, esquecer localização e reutilizar schema demais.
Fixar o contrato com testes
Schema é contrato de produto. Escreva um caso válido e alguns inválidos para impedir que mudanças futuras relaxem as regras sem intenção.
// 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 validação antes do DB, teste o objeto final antes de persistir. Isso captura mudanças de nomes de campos, enums ou transformações.
Armadilhas comuns
Primeira: tipos TypeScript não são validação em runtime. request.json() as ContactFormInput só convence o compilador.
Segunda: use parse e safeParse com intenção. Entrada de usuário geralmente precisa de safeParse e 400. Configuração pode derrubar o processo.
Terceira: não abuse de coerce. Ele é ótimo para query string e ambiente, mas perigoso quando a origem não é clara.
Quarta: não coloque efeitos colaterais em transform. Escrita no banco, email e analytics pertencem à lógica depois da validação.
Quinta: planeje mensagens e localização. Produto multilíngue precisa de chaves de mensagem ou camada de mapeamento.
Sexta: não compartilhe schemas por conveniência. Formulário, API, Webhook e DB insert podem parecer iguais, mas são contratos diferentes.
Consultoria e nota de verificação
Se suas validações estão espalhadas por formulários, webhooks, rotas API e banco, Claude Code Lab pode ajudar a separar schemas, criar prompts de revisão e definir testes. Para acompanhamento, use a página em inglês de consultoria e treinamento.
Os exemplos foram revisados em 2026-06-02 com base na documentação oficial do Zod e do Next.js Route Handlers. Eles assumem um projeto TypeScript com zod, react-hook-form, @hookform/resolvers e vitest. Em projetos reais, Masa adiciona autenticação, CSRF ou assinatura de webhook, restrições de banco e testes de falha para cada fronteira externa.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.