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.
Intisari
- Tipe TypeScript dihapus saat build. Hanya validasi runtime yang benar-benar memeriksa.
z.infermenurunkan tipe TypeScript dari skema — satu definisi, bukan dua yang bisa menyimpang.safeParsemengembalikan hasil;parsemelempar. Untuk data dari luar, biasanya yang pertama.- Validasi di batas: API masuk, form, webhook, parameter URL, variabel lingkungan.
z.coerceuntuk 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
parse | safeParse | |
|---|---|---|
| Gagal | Melempar ZodError | Mengembalikan { success: false, error } |
| Pakai untuk | Variabel lingkungan saat start — kamu ingin gagal cepat | Data 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
| Sumber | Validasi? |
|---|---|
| Respons API luar | Ya |
| Body webhook Midtrans | Ya — setelah verifikasi tanda tangan |
| Parameter URL & query string | Ya |
| Form | Ya — lewat Actions |
| Variabel lingkungan | Ya — saat start |
| Isi cookie | Ya — pembaca bisa menyuntingnya |
| Hasil kueri Kysely | Tidak — tipenya sudah dari skema database |
| Kolom JSON di MySQL | Ya — 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.