Claude Code से Form Validation: React Hook Form और Zod Guide
Claude Code से React Hook Form, Zod, server validation, API errors, i18n, accessibility और tests लागू करें।
पहले data contract तय करें
Form validation सिर्फ खाली field पर लाल message दिखाने का काम नहीं है। Trial signup, contact form, booking, checkout, onboarding और admin screen में form सीधे revenue और operations से जुड़ा होता है। Form दिखने में ठीक हो सकता है, फिर भी double submit, number को string की तरह भेजना, server error न दिखाना, या screen reader के लिए error न बताना जैसी गलती कर सकता है।
Claude Code इस काम में मदद करता है, क्योंकि validation structured काम है। React Hook Form form state और submit संभालता है। Zod schema लिखता है, यानी data contract। resolver React Hook Form और Zod को जोड़ता है। Server-side validation का मतलब है कि API browser से आए JSON को फिर से check करे। Error normalization का मतलब है Zod error, business rule error और invalid JSON को एक format में बदलना। i18n internationalization है, और accessibility का मतलब है कि keyboard और assistive technology users भी error समझ सकें।
इस guide में B2B contact form के लिए React Hook Form, Zod, server validation, API error normalization, accessibility, i18n और tests हैं। Related articles के लिए React Hook Form with Claude Code और Zod validation with Claude Code देखें। Official references भी check करें: Claude Code overview, React Hook Form useForm, React Hook Form Resolvers, Zod, Next.js Route Handlers, और Testing Library।
flowchart TD
A["User input"] --> B["React Hook Form"]
B --> C["zodResolver"]
C --> D{"Client valid?"}
D -->|No| E["Accessible field errors"]
D -->|Yes| F["POST /api/contact"]
F --> G["Server Zod validation"]
G --> H["Normalize API errors"]
H --> I["setError or root message"]
G --> J["Persist or notify"]
Use cases अलग हैं, rules भी अलग होंगे
Claude Code को सिर्फ “validation add करो” न कहें। Form का business context दें।
| Use case | जरूरी validation | आम गलती |
|---|---|---|
| SaaS trial | Work email, seats, plan, terms acceptance | Personal email allow है या नहीं, यह तय न करना |
| Contact form | Name, email, category, message length, spam URLs | केवल client validation करना |
| Admin user edit | Role, permission, immutable ID, allowed fields | UI में field छिपाना, लेकिन API में accept करना |
| Booking या payment | Date, quantity, phone, address, inventory | Double click से duplicate booking बनना |
Prompt ऐसा लिखें:
Implement a contact form.
Only change the files in this feature.
Use React Hook Form, Zod, and @hookform/resolvers/zod.
Validate on the client and again in the API using the same schema.
Normalize API failures as { ok: false, errors: [{ path, message }] }.
Include duplicate-submit prevention, aria-invalid, aria-describedby, role="alert",
i18n message keys, and Vitest/Testing Library tests.
Use copy-pasteable TypeScript and React, not pseudocode.
इससे Claude Code happy path के साथ failure path भी implement करता है। Review में type, server boundary, error display, double submit, accessibility और tests देखें।
Copy-paste Zod schema
Schema final Hindi या English text नहीं लौटाता, बल्कि message key लौटाता है। UI language dictionary से text दिखाता है।
// src/features/contact/contactSchema.ts
import { z } from "zod";
export const contactSchema = z
.object({
name: z.string().trim().min(1, "validation.name.required").max(60, "validation.name.tooLong"),
email: z.string().trim().min(1, "validation.email.required").email("validation.email.invalid"),
plan: z.enum(["starter", "team", "enterprise"], { message: "validation.plan.invalid" }),
seats: z
.number({ message: "validation.seats.number" })
.int("validation.seats.integer")
.min(1, "validation.seats.min")
.max(200, "validation.seats.max"),
message: z.string().trim().min(20, "validation.message.tooShort").max(1000, "validation.message.tooLong"),
locale: z.enum(["ja", "en"], { message: "validation.locale.invalid" }),
agreeToTerms: z.boolean().refine((value) => value === true, "validation.terms.required"),
})
.strict();
export type ContactFormData = z.infer<typeof contactSchema>;
export const defaultContactValues: ContactFormData = {
name: "",
email: "",
plan: "starter",
seats: 1,
message: "",
locale: "en",
agreeToTerms: false,
};
seats को number रखा गया है। HTML number input भी गलत wiring में string बन सकता है। React Hook Form में valueAsNumber लगाएं, या raw FormData के लिए API schema में z.coerce.number() इस्तेमाल करें।
Server validation और API error normalization
Client validation user experience है। System safety server पर आती है। API body को unknown मानें और Zod के बाद ही business logic चलाएं।
// src/app/api/contact/route.ts
import { z } from "zod";
import { contactSchema, type ContactFormData } from "@/features/contact/contactSchema";
type FieldPath = keyof ContactFormData | "root";
export type ApiFieldError = {
path: FieldPath;
message: string;
};
export type ContactApiResponse =
| { ok: true; id: string }
| { ok: false; errors: ApiFieldError[] };
function normalizeZodError(error: z.ZodError): ApiFieldError[] {
return error.issues.map((issue) => {
const firstPath = issue.path[0];
return {
path: typeof firstPath === "string" ? (firstPath as FieldPath) : "root",
message: issue.message,
};
});
}
function jsonResponse(body: ContactApiResponse, status: number): Response {
return Response.json(body, { status });
}
async function isBlockedDomain(email: string): Promise<boolean> {
return email.toLowerCase().endsWith("@example.invalid");
}
export async function POST(request: Request): Promise<Response> {
let body: unknown;
try {
body = await request.json();
} catch {
return jsonResponse({ ok: false, errors: [{ path: "root", message: "validation.json.invalid" }] }, 400);
}
const parsed = contactSchema.safeParse(body);
if (!parsed.success) {
return jsonResponse({ ok: false, errors: normalizeZodError(parsed.error) }, 422);
}
if (await isBlockedDomain(parsed.data.email)) {
return jsonResponse({ ok: false, errors: [{ path: "email", message: "validation.email.blocked" }] }, 409);
}
// Replace this with database insert, CRM sync, or email notification.
const id = crypto.randomUUID();
return jsonResponse({ ok: true, id }, 201);
}
अब UI को सिर्फ path और message संभालने हैं। Error Zod से आया या business rule से, UI को फर्क नहीं पड़ता।
React Hook Form component
यह component isSubmitting से duplicate submit रोकता है, ARIA attributes देता है, और API field error वापस field में रखता है।
// src/features/contact/ContactForm.tsx
"use client";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { contactSchema, defaultContactValues, type ContactFormData } from "./contactSchema";
import type { ApiFieldError, ContactApiResponse } from "@/app/api/contact/route";
const messages = {
en: {
"validation.name.required": "Enter your name.",
"validation.name.tooLong": "Name must be 60 characters or fewer.",
"validation.email.required": "Enter your email address.",
"validation.email.invalid": "Enter a valid email address.",
"validation.email.blocked": "This email domain is not allowed.",
"validation.plan.invalid": "Choose a plan.",
"validation.seats.number": "Seats must be a number.",
"validation.seats.integer": "Seats must be an integer.",
"validation.seats.min": "Seats must be at least 1.",
"validation.seats.max": "Seats must be 200 or fewer.",
"validation.message.tooShort": "Message must be at least 20 characters.",
"validation.message.tooLong": "Message must be 1000 characters or fewer.",
"validation.locale.invalid": "Locale is invalid.",
"validation.terms.required": "You must agree to the terms.",
"validation.json.invalid": "The submitted body could not be read.",
"form.submitError": "Submit failed. Please try again later.",
},
} as const;
type Locale = keyof typeof messages;
const formFields = ["name", "email", "plan", "seats", "message", "locale", "agreeToTerms"] as const;
type FormField = (typeof formFields)[number];
function t(locale: Locale, key: string): string {
const table = messages[locale] as Record<string, string>;
return table[key] ?? key;
}
function isFormField(path: ApiFieldError["path"]): path is FormField {
return formFields.includes(path as FormField);
}
export function ContactForm({ locale = "en" }: { locale?: Locale }) {
const [serverMessage, setServerMessage] = useState<string | null>(null);
const { register, handleSubmit, setError, reset, formState: { errors, isSubmitting } } =
useForm<ContactFormData>({ resolver: zodResolver(contactSchema), defaultValues: { ...defaultContactValues, locale }, mode: "onBlur" });
async function onValidSubmit(values: ContactFormData) {
setServerMessage(null);
const response = await fetch("/api/contact", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(values) });
const result = (await response.json()) as ContactApiResponse;
if (!response.ok || !result.ok) {
const apiErrors = result.ok ? [] : result.errors;
for (const error of apiErrors) {
if (isFormField(error.path)) setError(error.path, { type: "server", message: t(locale, error.message) });
else setServerMessage(t(locale, error.message));
}
if (apiErrors.length === 0) setServerMessage(t(locale, "form.submitError"));
return;
}
reset({ ...defaultContactValues, locale });
}
return (
<form onSubmit={handleSubmit(onValidSubmit)} noValidate>
{serverMessage ? <p role="alert" aria-live="assertive">{serverMessage}</p> : null}
<label htmlFor="contact-name">Name</label>
<input id="contact-name" aria-invalid={Boolean(errors.name)} aria-describedby={errors.name ? "contact-name-error" : undefined} {...register("name")} />
{errors.name?.message ? <p id="contact-name-error" role="alert">{t(locale, errors.name.message)}</p> : null}
<label htmlFor="contact-email">Email address</label>
<input id="contact-email" type="email" aria-invalid={Boolean(errors.email)} aria-describedby={errors.email ? "contact-email-error" : undefined} {...register("email")} />
{errors.email?.message ? <p id="contact-email-error" role="alert">{t(locale, errors.email.message)}</p> : null}
<label htmlFor="contact-plan">Plan</label>
<select id="contact-plan" {...register("plan")}><option value="starter">Starter</option><option value="team">Team</option><option value="enterprise">Enterprise</option></select>
<label htmlFor="contact-seats">Seats</label>
<input id="contact-seats" type="number" min={1} max={200} {...register("seats", { valueAsNumber: true })} />
<label htmlFor="contact-message">Message</label>
<textarea id="contact-message" rows={6} {...register("message")} />
<label><input type="checkbox" {...register("agreeToTerms")} />I agree to the terms</label>
{errors.agreeToTerms?.message ? <p role="alert">{t(locale, errors.agreeToTerms.message)}</p> : null}
<button type="submit" disabled={isSubmitting} aria-busy={isSubmitting}>{isSubmitting ? "Submitting..." : "Submit"}</button>
</form>
);
}
Production app में labels और buttons भी translate करें। Example छोटा रखने के लिए labels English हैं।
Tests, failure examples और result
// src/features/contact/ContactForm.test.tsx
import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ContactForm } from "./ContactForm";
import { contactSchema } from "./contactSchema";
const validInput = {
name: "Masa",
email: "masa@example.com",
plan: "team",
seats: 3,
message: "I want to improve validation in a Claude Code workflow.",
locale: "en",
agreeToTerms: true,
} as const;
it("accepts valid schema input", () => {
expect(contactSchema.safeParse(validInput).success).toBe(true);
});
it("rejects string seats", () => {
expect(contactSchema.safeParse({ ...validInput, seats: "3" }).success).toBe(false);
});
it("does not submit invalid form", async () => {
const fetchMock = vi.spyOn(globalThis, "fetch");
render(<ContactForm locale="en" />);
await userEvent.click(screen.getByRole("button", { name: "Submit" }));
expect(await screen.findByText("Enter your name.")).toBeInTheDocument();
expect(fetchMock).not.toHaveBeenCalled();
});
Common failures हैं: client-only validation, double submit, number और checkbox type drift, server error न दिखना, accessibility को सिर्फ red text मानना, और Claude Code से scope से बाहर files बदलवा देना।
Form conversion path है। Trial form टूटे तो SEO और ads traffic waste होता है। Contact form error साफ न हो तो lead खो जाता है। ClaudeCodeLab की Claude Code training और consulting में real repository पर prompt, schema, tests और review checklist बनाए जा सकते हैं।
Masa के test में first version अच्छा दिखा, पर seats API में string गया और blocked domain error email field में नहीं दिखा। valueAsNumber, normalized API errors और Testing Library regression tests जोड़ने के बाद दोनों issues दोबारा पकड़े जा सके। Message keys से Japanese और English UI को एक ही server response से verify करना आसान हुआ।
Summary
Claude Code से form validation बनाते समय React Hook Form, Zod, server validation, API error normalization, accessibility, i18n और tests को एक साथ रखें। Form external input की boundary है। Boundary जितनी साफ होगी, generated code उतना भरोसेमंद होगा।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code Obsidian to CLAUDE.md workflow: context बार-बार न समझाएं
Obsidian notes को CLAUDE.md operating notes में बदलकर Claude Code sessions को resume करना आसान बनाएं.
Claude Code Revenue CTA Routing: article से PDF, Gumroad और consultation तक
Reader intent के आधार पर free PDF, Gumroad products और consultation तक CTA route करने वाला workflow.
Claude Code टीम हैंडऑफ नियम: review proof, permissions, rollback और revenue path
Claude Code टीम काम के लिए evidence, permission rules, rollback, free PDF, Gumroad और consultation path वाला handoff.