Kembali ke Blog
Tutorial#zod#validasi#typescript#react hook form#backend

Zod: Validasi Data TypeScript yang Seharusnya Sudah Anda Pakai dari Dulu

Zod adalah schema validation library yang mengubah cara developer TypeScript handle input validation. Type-safe, composable, dan integrasinya mulus dengan Next.js, React Hook Form, dan tRPC.

Muhamad Putra Aulia Hidayat

Muhamad Putra Aulia Hidayat

6 Maret 20263 menit baca

Zod: Validasi Data yang Type-Safe dan Ekspresif

Kalau kamu masih validasi input secara manual dengan if-else berantai, ini saatnya beralih ke Zod. Library ini mengubah validasi dari pekerjaan membosankan jadi bagian yang menyenangkan dari development.

Kenapa Zod?

  • Type inference — schema otomatis menghasilkan TypeScript type
  • Composable — schema bisa dikombinasikan dan di-reuse
  • Zero dependencies — ringan dan tanpa bloat
  • Great error messages — pesan error yang informatif
  • Ekosistem luas — terintegrasi dengan React Hook Form, tRPC, SWR, dll

Dasar-Dasar Zod

import { z } from "zod"

// Schema sederhana
const EmailSchema = z.string().email("Email tidak valid")
const AgeSchema = z.number().int().min(17).max(100)

// Object schema
const UserSchema = z.object({
  name: z.string().min(2, "Nama minimal 2 karakter").max(50),
  email: z.string().email("Email tidak valid"),
  phone: z.string().regex(/^08[0-9]{8,11}$/, "Format HP tidak valid").optional(),
  age: z.number().int().min(17).max(100),
  role: z.enum(["admin", "user", "moderator"]).default("user"),
})

// Infer TypeScript type secara otomatis
type User = z.infer<typeof UserSchema>
// Sama dengan:
// type User = {
//   name: string;
//   email: string;
//   phone?: string;
//   age: number;
//   role: "admin" | "user" | "moderator";
// }

Parse vs SafeParse

// parse() - throw error kalau invalid
try {
  const user = UserSchema.parse(formData)
  // user sudah pasti valid dan type-safe
} catch (error) {
  if (error instanceof z.ZodError) {
    console.log(error.flatten().fieldErrors)
  }
}

// safeParse() - return result object, tidak throw
const result = UserSchema.safeParse(formData)
if (!result.success) {
  const errors = result.error.flatten().fieldErrors
  return { errors }
}
const user = result.data // Type: User

Schema Lanjutan

// Array
const TagsSchema = z.array(z.string()).min(1).max(10)

// Union
const IdSchema = z.union([z.string().uuid(), z.number().int().positive()])

// Transform - ubah data saat validasi
const PriceSchema = z.string().transform(val => parseFloat(val))
// Input: "25000" → Output: 25000 (number)

// Refine - validasi custom
const PasswordSchema = z.string()
  .min(8)
  .refine(
    (val) => /[A-Z]/.test(val) && /[0-9]/.test(val),
    "Password harus mengandung huruf kapital dan angka"
  )

// SuperRefine - validasi yang bergantung pada field lain
const SignupSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string()
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Password tidak sama",
      path: ["confirmPassword"]
    })
  }
})

Integrasi dengan React Hook Form

"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"

const ContactSchema = z.object({
  name: z.string().min(2, "Nama minimal 2 karakter"),
  email: z.string().email("Email tidak valid"),
  message: z.string().min(20, "Pesan minimal 20 karakter"),
})

type ContactForm = z.infer<typeof ContactSchema>

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<ContactForm>({
    resolver: zodResolver(ContactSchema)
  })

  const onSubmit = async (data: ContactForm) => {
    // data sudah pasti valid dan type-safe
    await submitContact(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="Nama" />
      {errors.name && <p className="text-red-500">{errors.name.message}</p>}

      <input {...register("email")} placeholder="Email" />
      {errors.email && <p className="text-red-500">{errors.email.message}</p>}

      <textarea {...register("message")} placeholder="Pesan" />
      {errors.message && <p className="text-red-500">{errors.message.message}</p>}

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

Validasi di API Route

// app/api/products/route.ts
import { z } from "zod"

const CreateProductSchema = z.object({
  name: z.string().min(3).max(100),
  price: z.number().positive("Harga harus lebih dari 0"),
  stock: z.number().int().min(0),
  category_id: z.string().uuid("Category ID tidak valid"),
  description: z.string().max(1000).optional(),
})

export async function POST(req: Request) {
  const body = await req.json()
  const result = CreateProductSchema.safeParse(body)

  if (!result.success) {
    return Response.json(
      {
        error: "Validation failed",
        details: result.error.flatten().fieldErrors
      },
      { status: 400 }
    )
  }

  const product = await createProduct(result.data)
  return Response.json(product, { status: 201 })
}

Zod adalah salah satu library yang setelah kamu pakai, tidak akan mau kembali ke cara lama.

zodvalidasitypescriptreact hook formbackend

Newsletter Digital Uptime

Tips teknologi & bisnis mingguan

Bergabung dengan 2,500+ subscriber yang mendapatkan insight teknologi, tutorial development, dan tips bisnis digital langsung ke inbox mereka setiap minggu.

Tidak ada spam. Unsubscribe kapan saja.

Artikel Terkait

Kami menggunakan cookies untuk meningkatkan pengalaman Anda di website ini. Dengan melanjutkan, Anda menyetujui penggunaan cookies sesuai Kebijakan Privasi kami.