Use Cases (Diperbarui: 2/6/2026)

Validasi Zod dengan Claude Code untuk TypeScript

Bangun validasi form, API, env variables, webhook, dan test dengan Claude Code serta Zod.

Validasi Zod dengan Claude Code untuk TypeScript

Mengapa Zod perlu ada di batas runtime

TypeScript membantu saat menulis kode, tetapi tidak otomatis memeriksa data yang datang saat aplikasi berjalan. Form dari browser, body API, Webhook payload, process.env, atau object sebelum masuk database bisa saja tidak sesuai bentuk yang kita harapkan. Runtime validation berarti memeriksa nilai itu sebelum business logic mempercayainya. Zod memberi schema, yaitu kontrak data, dan dari schema itu kita bisa mengambil type TypeScript.

Claude Code cocok untuk pekerjaan ini karena validasi bersifat terstruktur: field, type, batas panjang, enum, pesan error, lokasi penggunaan, dan test. Jika prompt hanya “buat validasi”, hasilnya sering terlalu umum. Jika prompt menyebut “form, API request/response, environment variables, Webhook payload, validasi sebelum DB insert, dan test”, Claude Code punya batas yang jelas. Untuk detail perilaku, cek dokumentasi resmi Zod dan dokumentasi Next.js Route Handlers.

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

Aturan utamanya: external input adalah unknown sampai lolos Zod. Jangan langsung menulis as SomeType dan menganggap data sudah aman.

Use case yang perlu diberikan ke Claude Code

Setiap boundary punya kebutuhan berbeda. Form perlu pesan yang mudah dipahami user. API perlu response 400. Environment variables perlu gagal saat startup. Webhook perlu signature verification sebelum payload diproses.

Use caseEntryYang dilindungi Zod
FormInput browserEmpty string, email, panjang teks, consent
API request/responserequest.json() dan response JSONPayload invalid, kontrak response, status
Environment variablesprocess.envSecret hilang, URL invalid, port
Webhook payloadPOST dari pihak ketigaEvent type, object ID, amount, signature flow
Sebelum DB insertObject setelah mapping appField yang bisa disimpan, enum, ID wajib

Masukkan tabel ini ke prompt Claude Code. Jangan memaksa satu schema untuk semua layer. Form bisa punya checkbox atau field konfirmasi; DB insert punya kontrak berbeda. Share bagian kecil seperti emailSchema atau idSchema, bukan seluruh object tanpa alasan. Untuk form, baca juga panduan React Hook Form. Untuk API type-safe, lihat panduan tRPC.

Membuat schema Zod dasar

Contoh berikut memvalidasi contact form. Kodenya bisa disalin dan disesuaikan. Saat meminta Claude Code membuatnya, jelaskan bahasa pesan error, field wajib, batas panjang, pilihan enum, dan bahwa field database seperti id atau createdAt tidak boleh masuk schema form.

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

export const contactFormSchema = z.object({
  name: z
    .string()
    .trim()
    .min(1, "Masukkan nama")
    .max(80, "Nama maksimal 80 karakter"),
  email: z
    .string()
    .trim()
    .email("Masukkan email yang valid"),
  plan: z.enum(["trial", "team", "enterprise"]),
  message: z
    .string()
    .trim()
    .min(10, "Pesan minimal 10 karakter")
    .max(2000, "Pesan maksimal 2000 karakter"),
  agreedToPolicy: z
    .boolean()
    .refine((value) => value, "Persetujuan kebijakan privasi wajib diisi"),
});

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

trim() mencegah input berisi spasi saja. z.enum membatasi string ke pilihan yang dikenal. z.infer mengurangi risiko type dan schema tidak sinkron.

Mengubah safeParse menjadi error API

parse melempar exception saat gagal, cocok untuk konfigurasi startup. safeParse mengembalikan hasil terkontrol, lebih cocok untuk form dan API yang perlu memberi response 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 };
}

Helper ini menyamakan format error. Untuk produk multi bahasa, message bisa diganti message key lalu diterjemahkan di UI.

Validasi request dan response di Next.js

Route Handler harus memvalidasi input sebelum business logic. Response schema juga berguna untuk menangkap perubahan kontrak yang tidak disengaja.

// 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: "Periksa kembali input Anda", errors: validated.errors },
      { status: validated.status },
    );
  }

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

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

Untuk webhook, urutannya harus jelas: verify signature, validasi payload schema, lalu jalankan business logic. Minta Claude Code memisahkan verifySignature, webhookPayloadSchema, dan handleWebhookEvent.

Validasi environment variables saat startup

Environment variables adalah string atau undefined. Jika DATABASE_URL hilang, aplikasi sebaiknya gagal saat startup, bukan saat request pertama masuk.

// 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() cocok untuk PORT karena sumbernya string. Jangan memakai coercion untuk semua JSON body. Jika source tidak jelas, coercion bisa membuat input buruk terlihat valid.

Integrasi dengan react-hook-form

Client validation memperbaiki pengalaman user, tetapi tidak menggantikan server validation. Browser bisa dilewati.

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

Saat Claude Code mengubah UI, tulis aturan bahwa validasi server tidak boleh dihapus. Ini mencegah form terlihat bagus tetapi API boundary menjadi lemah.

Prompt review untuk Claude Code

Setelah implementasi, gunakan Claude Code sebagai reviewer khusus validasi.

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.

Prompt ini mengecek masalah penting: terlalu percaya TypeScript type, salah memilih parse atau safeParse, terlalu banyak coerce, side effect di transform, pesan error belum siap localization, dan schema dipakai ulang terlalu luas.

Mengunci kontrak dengan test

Schema adalah kontrak produk. Tulis satu valid case dan beberapa invalid case agar perubahan berikutnya tidak melemahkan aturan tanpa sadar.

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

Untuk validasi sebelum DB, test object final sebelum disimpan. Ini membantu menangkap perubahan field name, enum, atau mapping.

Jebakan umum

Pertama, TypeScript type bukan runtime validation. request.json() as ContactFormInput hanya membuat compiler percaya.

Kedua, bedakan parse dan safeParse. User input dan API biasanya memakai safeParse agar bisa mengembalikan 400. Environment config boleh menghentikan proses.

Ketiga, jangan berlebihan memakai coerce. Cocok untuk query string dan env, tetapi berbahaya untuk input yang sumbernya tidak jelas.

Keempat, jangan taruh side effect di transform. DB write, email, dan analytics harus terjadi setelah validasi.

Kelima, pikirkan error message dan localization sejak awal. Produk multi bahasa lebih aman memakai message key atau mapping layer.

Keenam, jangan over-reuse schema. Form, API, Webhook, dan DB insert mirip, tetapi kontraknya berbeda.

Konsultasi dan catatan verifikasi

Jika validasi sudah tersebar di banyak form, webhook, API route, dan kode DB, Claude Code Lab dapat membantu menyusun layer schema, prompt review, dan strategi test. Untuk pendampingan proyek, gunakan halaman Inggris consulting and training.

Contoh dalam artikel ini ditinjau pada 2026-06-02 berdasarkan dokumentasi resmi Zod dan Next.js Route Handlers. Kode mengasumsikan proyek TypeScript dengan zod, react-hook-form, @hookform/resolvers, dan vitest. Dalam proyek nyata, Masa menambahkan authentication, CSRF atau webhook signature verification, DB constraints, dan failing tests untuk setiap external boundary.

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

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.