Nextjs Forms Zod Validation File Inputs Multipart Handling
Selesai membaca dokumentasi ini?
Kembali ke rujukan utama Next.js untuk melanjutkan topik lainnya.
Kembali ke rujukan utama Next.js untuk melanjutkan topik lainnya.
Di Next.js App Router, form bukan cuma urusan onSubmit dan preventDefault. Begitu form mulai menerima input kompleks—email, password, nested data, file upload, atau multipart/form-data—kita perlu memisahkan tanggung jawab dengan jelas:
Zod cocok dipakai untuk menyatukan skema validasi di client dan server. Tapi penting: schema yang sama tidak otomatis berarti trust yang sama. Client validation hanya membantu pengalaman pengguna; server tetap sumber kebenaran.
Validasi client cukup kalau tujuan utamanya adalah feedback instan dan data yang divalidasi tidak dipakai untuk keputusan final.
Contoh penggunaan yang aman:
Client validation cocok untuk:
Tapi client validation tidak boleh dianggap final karena:
Server validation wajib untuk semua data yang:
Server harus memvalidasi lagi walaupun client sudah pakai Zod. Alasannya sederhana:
Contoh server-side checks yang wajib:
Kalau ada efek permanen, server validation selalu wajib.
Pola yang sehat di App Router biasanya seperti ini:
FormDataBiasanya ada dua level schema:
Kenapa perlu dibedakan? Karena input HTML hampir selalu berupa string, walau niatnya angka, tanggal, atau boolean.
Contoh sederhana:
import { z } from "zod";
export const profileSchema = z.object({
name: z.string().trim().min(2, "Nama minimal 2 karakter"),
email: z.string().trim().email(
z.coerce.number() membantu saat data datang dari form HTML. Tanpa coercion, age biasanya masih string.
trim() bagus untuk mencegah spasi kosong lolos validasicoerce membantu parsing input form, tapi tetap harus divalidasi ulangFile input tidak diproses seperti field text biasa.
Di browser, file input menghasilkan File object, dan ketika dikirim biasanya masuk ke request sebagai bagian dari multipart form data.
Hal yang perlu diperhatikan:
FormData menyimpan file sebagai File atau BlobContoh schema untuk file di server:
import { z } from "zod";
export const avatarSchema = z.object({
avatar: z
.instanceof(File)
.refine((file) => file.size > 0, "File kosong")
Untuk multiple files, validasinya biasanya berbentuk array:
const gallerySchema = z.object({
images: z.array(z.instanceof(File)).min(1).max(5),
});Kalau form mengandung file, gunakan multipart/form-data.
Pada HTML form:
<form action={saveProfile} encType="multipart/form-data">
<input name="name" />
<input name="email" />
<input name="avatar" type="file" />
<
Tanpa encType="multipart/form-data", file tidak akan terkirim dengan benar.
Di server, data biasanya dibaca sebagai FormData:
"use server";
import { profileSchema } from "./schema";
export async function saveProfile(formData: FormData) {
const raw = {
name: formData.get("name"),
Saat field text dan file dikirim bersamaan, semua masuk lewat FormData.
Pola yang umum:
formData.get(...)Jangan langsung anggap formData.get("avatar") pasti file. Bisa saja null atau string kalau input salah nama atau payload dimanipulasi.
Keduanya bisa dipakai untuk form.
Cocok kalau:
Kelebihan:
useActionState atau pending stateBatasan:
Cocok kalau:
fetch manual dari clientKelebihan:
Batasan:
Client memeriksa field sebelum submit:
const clientSchema = z.object({
name: z.string().trim().min(2),
email: z.string().trim().email(),
});Gunakan ini untuk menampilkan error lebih cepat, bukan untuk menggantikan server.
<form action={saveProfile} encType="multipart/form-data">
{/* field input */}
</form>export async function saveProfile(formData: FormData) {
const avatar = formData.get("avatar");
const parsed = profileSchema.safeParse({
name: formData.get("name"
Simpan data final hanya setelah:
Ini jebakan paling umum. Client validation membantu UX, tetapi server tetap harus memvalidasi ulang.
encType="multipart/form-data"Tanpa ini, file input tidak akan terkirim sesuai harapan.
FormData adalah stringFormData.get() bisa mengembalikan:
stringFilenullKalau langsung dipaksa ke string, bug-nya bisa halus dan sulit dilacak.
safeParseparse() melempar exception. Untuk form production, safeParse() sering lebih nyaman karena error bisa dikembalikan sebagai state terstruktur.
Field text enak divalidasi dengan schema. File biasanya butuh pengecekan tambahan karena ukuran, MIME type, dan content sniffing.
Nama file dari user tidak aman sebagai identitas final.
Simpan dengan:
Di App Router, form state bisa terasa reset kalau:
Untuk form kompleks, pertimbangkan:
useActionState atau useFormState jika cocokUntuk file besar, pikirkan batas runtime, timeout, dan alur upload.
Kalau file makin besar atau butuh progress bar, sering kali lebih sehat memakai:
Ada tiga pola yang umum:
Cocok untuk:
Risiko:
Cocok untuk production.
Contoh: S3, R2, GCS, atau storage lain.
Keuntungannya:
Cocok kalau:
Dalam pola ini, server aplikasi hanya mengeluarkan signed URL atau token upload, lalu menyimpan metadata setelah upload selesai.
trim() sebelum validasi final.FormData untuk submit form yang mengandung file.FormData, Server Action/Route Handler, dan re-render behavior