← Semua pembelajaran / Astro Nol → Portal Berita
Fase 3 · Jalur API

Zod — memvalidasi data yang datang dari luar

Setiap tempat data masuk ke sistemmu adalah tempat tipe TypeScript berhenti melindungimu. Zod mengisi celah itu, dan menghasilkan tipe sekalian.

Sumber asli zod.dev Resmi Rangkuman ~8 menit baca

Intisari

  • Tipe TypeScript dihapus saat build. Hanya validasi runtime yang benar-benar memeriksa.
  • z.infer menurunkan tipe TypeScript dari skema — satu definisi, bukan dua yang bisa menyimpang.
  • safeParse mengembalikan hasil; parse melempar. Untuk data dari luar, biasanya yang pertama.
  • Validasi di batas: API masuk, form, webhook, parameter URL, variabel lingkungan.
  • z.coerce untuk parameter URL yang selalu string tapi seharusnya angka.

Kenapa ini bukan opsional

interface Perusahaan { kode: string; nama: string; kapitalisasi: number; }

const p = await ambilJson<Perusahaan>(URL);

p.kapitalisasi.toLocaleString("id-ID");
// TypeError kalau API mengirim null, string, atau tidak mengirimnya sama sekali.
// TypeScript sudah bilang "aman". TypeScript berbohong.

ambilJson<Perusahaan> hanya menempelkan label. Tidak ada satu pun pemeriksaan yang terjadi. Ini persis kelas bug yang membuat halaman produksi menampilkan 500 saat vendor data mengubah formatnya diam-diam pada hari Jumat sore.

Skema sebagai satu-satunya sumber

// src/lib/skema/perusahaan.ts
import { z } from "zod";

export const SkemaPerusahaan = z.object({
  kode: z.string().length(4),
  nama: z.string().min(1),
  sektor: z.string().nullable(),
  kapitalisasi: z.number().nonnegative(),
  diperbaruiPada: z.coerce.date(),          // string ISO → Date
  premium: z.boolean().default(false),
});

export type Perusahaan = z.infer<typeof SkemaPerusahaan>;

z.infer itu kuncinya. Kamu menulis skema sekali dan mendapat tipe TypeScript gratis. Tidak mungkin skema dan tipe menyimpang, karena hanya ada satu.

Memakainya di batas

// src/lib/perusahaan.ts
import { SkemaPerusahaan, type Perusahaan } from "./skema/perusahaan";
import { ambilJson } from "./api";

export async function ambilPerusahaan(kode: string): Promise<Perusahaan | null> {
  const mentah = await ambilJson<unknown>(`${BASE}/perusahaan/${kode}`);

  const hasil = SkemaPerusahaan.safeParse(mentah);

  if (!hasil.success) {
    console.error(JSON.stringify({
      level: "error",
      pesan: "bentuk data perusahaan berubah",
      kode,
      masalah: hasil.error.issues,
    }));
    return null;
  }

  return hasil.data;
}

Perhatikan ambilJson<unknown>, bukan ambilJson<Perusahaan>. unknown memaksa kamu memvalidasi sebelum bisa menyentuh apa pun — compiler menolak mentah.kode sampai kamu membuktikan bentuknya. Itu tepatnya perilaku yang kamu inginkan di batas sistem.

Dan ketika vendor mengubah format, yang terjadi adalah: satu baris log terstruktur dengan rincian field mana yang salah, dan halaman yang merender tanpa data perusahaan — bukan 500 untuk semua pembaca.

parse vs safeParse

parsesafeParse
GagalMelempar ZodErrorMengembalikan { success: false, error }
Pakai untukVariabel lingkungan saat start — kamu ingin gagal cepatData dari luar saat runtime
// src/lib/env.ts — gagal saat container start, bukan saat request pertama
import { z } from "zod";

const SkemaEnv = z.object({
  DB_HOST_RW: z.string().min(1),
  DB_HOST_RO: z.string().min(1),
  DB_USER: z.string().min(1),
  DB_PASSWORD: z.string().min(1),
  MIDTRANS_SERVER_KEY: z.string().startsWith("Mid-server-"),
  ASTRO_KEY: z.string().min(1),
});

export const env = SkemaEnv.parse(process.env);

Ini pagar deploy yang paling murah yang bisa kamu pasang. Kalau satu variabel lingkungan terlupa di task definition, container gagal start dengan pesan yang menyebut nama variabelnya — dan ECS membatalkan deploy sebelum satu pun pembaca terkena. Tanpa ini, container-nya start dengan senang hati lalu gagal misterius saat request pertama yang membutuhkannya, mungkin berjam-jam kemudian.

Parameter URL selalu string

const SkemaKueri = z.object({
  hal: z.coerce.number().int().positive().max(500).default(1),
  batas: z.coerce.number().int().positive().max(100).default(20),
  kategori: z.enum(["semua", "ekonomi", "pasar", "teknologi"]).default("semua"),
  urut: z.enum(["terbaru", "populer"]).default("terbaru"),
});

export const GET: APIRoute = async ({ url }) => {
  const hasil = SkemaKueri.safeParse(Object.fromEntries(url.searchParams));

  if (!hasil.success) {
    return new Response(
      JSON.stringify({ error: "Parameter tidak valid", detail: hasil.error.issues }),
      { status: 400, headers: { "Content-Type": "application/json" } },
    );
  }

  const { hal, batas, kategori, urut } = hasil.data;
  // Semuanya sudah bertipe benar dan dibatasi.
};

Satu skema menangani konversi tipe, nilai default, batas atas, dan daftar nilai yang sah. Bandingkan dengan berapa baris if yang dibutuhkan untuk melakukan hal yang sama di CI3 — dan berapa besar kemungkinan salah satunya terlupa.

.max(500) pada hal mencegah crawler menyeret database-mu. Tanpa batas, bot yang mengikuti tautan paginasi bisa meminta ?hal=999999 dan memaksa MySQL memindai jutaan baris untuk mengembalikan array kosong. Ini bukan hipotesis — ini pola beban yang rutin muncul di portal berita dengan paginasi terbuka.

Zod di Astro Actions

Actions memakai Zod secara bawaan lewat astro:schema (Fase 1). Skema yang sama bisa dipakai di kedua tempat:

// Dipakai action DAN endpoint
export const SkemaDaftar = z.object({
  email: z.string().email(),
  nama: z.string().min(2).max(100),
});

Yang harus divalidasi

SumberValidasi?
Respons API luarYa
Body webhook MidtransYa — setelah verifikasi tanda tangan
Parameter URL & query stringYa
FormYa — lewat Actions
Variabel lingkunganYa — saat start
Isi cookieYa — pembaca bisa menyuntingnya
Hasil kueri KyselyTidak — tipenya sudah dari skema database
Kolom JSON di MySQLYa — isinya tidak dijamin siapa pun

Latihan: buat src/lib/env.ts yang memvalidasi seluruh variabel lingkungan proyekmu dengan parse, dan impor dari src/lib/db.ts. Hapus satu variabel dari .env lalu jalankan pnpm dev — pastikan ia gagal seketika dengan nama variabel yang hilang tercetak jelas. Lalu buat skema untuk satu respons API yang kamu pakai, dan uji dengan JSON yang sengaja salah bentuk.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.