Nextjs Forms Zod Validation File Inputs Multipart Handling
Definisi
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:
- validasi di client untuk UX cepat
- validasi di server untuk keamanan dan integritas data
- multipart handling untuk file dan field biasa dalam satu request
- penyimpanan yang sesuai dengan jenis data: database, object storage, atau filesystem
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.
Kapan validasi di client cukup
Validasi client cukup kalau tujuan utamanya adalah feedback instan dan data yang divalidasi tidak dipakai untuk keputusan final.
Contoh penggunaan yang aman:
- menandai field wajib sebelum submit
- memeriksa format email, panjang password, atau pola username
- menonaktifkan tombol submit sampai input minimum terpenuhi
- memfilter pilihan UI yang sifatnya non-kritis
Client validation cocok untuk:
- mengurangi round-trip yang sia-sia
- mempercepat koreksi input
- memperbaiki UX pada form panjang
Tapi client validation tidak boleh dianggap final karena:
- bisa dimatikan user
- bisa dimanipulasi lewat DevTools atau request manual
- tidak memvalidasi konteks server seperti ownership, quota, duplikasi, atau otorisasi
Kapan server validation wajib
Server validation wajib untuk semua data yang:
- akan disimpan
- akan memicu perubahan state
- akan dipakai untuk otorisasi
- akan menentukan akses, biaya, quota, atau lifecycle data
- datang dari file upload atau request multipart
Server harus memvalidasi lagi walaupun client sudah pakai Zod. Alasannya sederhana:
- request bisa dikirim tanpa UI
- payload bisa dimodifikasi
- browser tidak bisa dipercaya sebagai boundary keamanan
Contoh server-side checks yang wajib:
- user benar-benar boleh mengubah record itu
- file bukan tipe yang dilarang
- ukuran file tidak melebihi limit
- nama file aman dari path traversal
- field relasional memang ada di database
- nilai enum benar-benar termasuk daftar yang diizinkan
Kalau ada efek permanen, server validation selalu wajib.
Pola kerja yang disarankan
Pola yang sehat di App Router biasanya seperti ini:
- User isi form di client
- Client validation memeriksa kesalahan dasar
- Form dikirim sebagai
FormData - Server menerima request di Server Action atau Route Handler
- Server melakukan parsing dan validation ulang
- Server menyimpan data ke database atau storage
- Server mengembalikan state error/success ke UI
Struktur validasi dengan Zod
Biasanya ada dua level schema:
- schema domain untuk data final yang disimpan
- schema form untuk data mentah dari input HTML
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("Email tidak valid"),
age: z.coerce.number().int().min(17, "Minimal 17 tahun"),
});z.coerce.number() membantu saat data datang dari form HTML. Tanpa coercion, age biasanya masih string.
Catatan penting
trim()bagus untuk mencegah spasi kosong lolos validasicoercemembantu parsing input form, tapi tetap harus divalidasi ulang- schema client dan server boleh sama, tapi eksekusinya tidak boleh bergantung pada client saja
File inputs: apa yang berbeda
File 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:
- file input tidak bisa di-set value-nya secara bebas dari JS untuk alasan keamanan
FormDatamenyimpan file sebagaiFileatauBlob- field file bisa kosong, satu file, atau banyak file tergantung atribut input
- validasi file harus cek ekstensi saja tidak cukup; cek MIME type dan ukuran juga
Contoh 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")
.refine((file) => file.size <= 2 * 1024 * 1024, "Maksimal 2 MB")
.refine(
(file) => ["image/jpeg", "image/png", "image/webp"].includes(file.type),
"Format file harus JPG, PNG, atau WEBP"
),
});Untuk multiple files, validasinya biasanya berbentuk array:
const gallerySchema = z.object({
images: z.array(z.instanceof(File)).min(1).max(5),
});Multipart handling di App Router
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" />
<button type="submit">Simpan</button>
</form>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"),
email: formData.get("email"),
age: formData.get("age"),
};
const parsed = profileSchema.safeParse(raw);
if (!parsed.success) {
return {
ok: false,
errors: parsed.error.flatten().fieldErrors,
};
}
// lanjut save
return { ok: true };
}Field biasa + file dalam satu request
Saat field text dan file dikirim bersamaan, semua masuk lewat FormData.
Pola yang umum:
- baca semua field dari
formData.get(...) - validasi field text dengan Zod
- validasi file secara eksplisit
- simpan file ke storage terpisah
- simpan metadata file ke database
Jangan langsung anggap formData.get("avatar") pasti file. Bisa saja null atau string kalau input salah nama atau payload dimanipulasi.
Server Action vs Route Handler
Keduanya bisa dipakai untuk form.
Server Action
Cocok kalau:
- form dipakai langsung dari UI React/Next.js
- alur submit sederhana
- ingin integrasi natural dengan App Router
Kelebihan:
- ergonomis untuk form native
- dekat dengan komponen UI
- mudah dipadukan dengan
useActionStateatau pending state
Batasan:
- tidak selalu cocok untuk upload yang sangat besar atau kebutuhan custom transport
- debugging payload multipart kadang terasa lebih abstrak
Route Handler
Cocok kalau:
- butuh kontrol request yang lebih eksplisit
- mau pakai
fetchmanual dari client - ingin endpoint yang juga bisa dipakai non-React consumer
Kelebihan:
- kontrol penuh atas request/response
- enak untuk API yang jelas batasnya
Batasan:
- lebih banyak boilerplate daripada Server Action
Contoh alur yang aman
1. Client validation
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.
2. Submit sebagai FormData
<form action={saveProfile} encType="multipart/form-data">
{/* field input */}
</form>3. Server validation
export async function saveProfile(formData: FormData) {
const avatar = formData.get("avatar");
const parsed = profileSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!parsed.success) {
return { ok: false, errors: parsed.error.flatten().fieldErrors };
}
if (!(avatar instanceof File)) {
return { ok: false, errors: { avatar: ["Avatar wajib diisi"] } };
}
// validasi file lanjut di sini
}4. Save
Simpan data final hanya setelah:
- schema lolos
- otorisasi lolos
- file valid
- storage siap
Jebakan umum di App Router forms
1. Mengandalkan client validation saja
Ini jebakan paling umum. Client validation membantu UX, tetapi server tetap harus memvalidasi ulang.
2. Lupa encType="multipart/form-data"
Tanpa ini, file input tidak akan terkirim sesuai harapan.
3. Mengira semua value dari FormData adalah string
FormData.get() bisa mengembalikan:
stringFilenull
Kalau langsung dipaksa ke string, bug-nya bisa halus dan sulit dilacak.
4. Tidak pakai safeParse
parse() melempar exception. Untuk form production, safeParse() sering lebih nyaman karena error bisa dikembalikan sebagai state terstruktur.
5. Tidak memisahkan validasi field dan file
Field text enak divalidasi dengan schema. File biasanya butuh pengecekan tambahan karena ukuran, MIME type, dan content sniffing.
6. Mengandalkan nama file dari user
Nama file dari user tidak aman sebagai identitas final.
Simpan dengan:
- UUID atau hash sebagai nama file storage
- metadata asli di database
- nama asli hanya sebagai display label jika perlu
7. Re-render yang membuat state form terasa hilang
Di App Router, form state bisa terasa reset kalau:
- komponen re-mount
- redirect terjadi terlalu cepat
- state pending/success tidak dipisahkan dengan rapi
Untuk form kompleks, pertimbangkan:
- state lokal untuk error
useActionStateatauuseFormStatejika cocok- optimistic update hanya untuk data yang aman dioptimalkan
8. Upload besar di Server Action tanpa strategi
Untuk file besar, pikirkan batas runtime, timeout, dan alur upload.
Kalau file makin besar atau butuh progress bar, sering kali lebih sehat memakai:
- direct upload ke storage
- signed URL
- lalu submit metadata ke server
Strategi penyimpanan file
Ada tiga pola yang umum:
1. Simpan langsung ke filesystem
Cocok untuk:
- development
- aplikasi kecil
- environment yang memang punya storage persisten
Risiko:
- tidak cocok untuk serverless yang ephemeral
- deploy multi-instance bisa bikin file hilang atau tidak sinkron
2. Simpan ke object storage
Cocok untuk production.
Contoh: S3, R2, GCS, atau storage lain.
Keuntungannya:
- lebih stabil untuk file besar
- lebih mudah diskalakan
- file bisa diakses via URL atau CDN
3. Upload langsung dari client ke storage
Cocok kalau:
- file besar
- butuh progress upload
- ingin mengurangi beban server aplikasi
Dalam pola ini, server aplikasi hanya mengeluarkan signed URL atau token upload, lalu menyimpan metadata setelah upload selesai.
Implementasi notes
- Buat schema yang dipakai ulang di client dan server, tapi jangan simpan kepercayaan pada client.
- Normalisasi input string dengan
trim()sebelum validasi final. - Gunakan
FormDatauntuk submit form yang mengandung file. - Jalankan validasi otorisasi sebelum save, bukan setelah save.
- Simpan file di storage yang sesuai dengan lingkungan deploy.
- Untuk error form, kembalikan struktur per-field agar UI mudah menampilkan pesan.
Ringkasan praktik baik
- Client validation: untuk UX
- Server validation: untuk kebenaran data dan keamanan
- Multipart: untuk field + file dalam satu request
- File handling: validasi tipe, ukuran, dan sumber; simpan dengan nama aman
- App Router forms: perhatikan
FormData, Server Action/Route Handler, dan re-render behavior
Referensi terkait
- Next.js Forms, Server Actions, Optimistic UI, dan Pending States
- Next.js Route Handlers, API Endpoints, Query/Body Parsing, dan Response Helpers
- HTML Forms, Inputs, Labels, Validation, dan Accessibility
- React.js Forms: Controlled vs Uncontrolled, Lifting State, Validation, dan Submit Flow
- PHP Validation, Form Requests, Redirects, dan Error Bags