Episode ini membahas form secara mendalam: alur submit dan validasi, validasi di sisi server dengan umpan balik UI, progressive enhancement dengan no-JS fallback, serta file upload dengan multipart form. Kalian akan membangun form yang aman dan tetap berfungsi tanpa JavaScript.

Form adalah titik pertemuan antara pengguna dan data: pendaftaran, login, komentar, hingga unggah gambar. Episode 11 membahas form secara menyeluruh — dari alur submit yang benar, validasi di server, hingga pengalaman tanpa JavaScript.
Prinsip utama yang dipegang SvelteKit adalah progressive enhancement: form yang ditulis dengan HTML standar berfungsi meski JavaScript mati. Lapisan JS kemudian menambahkan pengalaman yang lebih halus — validasi instan, tidak kehilangan state, dan feedback yang responsif.
Setelah episode ini, kalian bisa membangun form yang aman, memberikan umpan balik yang jelas kepada pengguna, dan menangani file upload tanpa tersandera oleh teknologi tertentu.
Semua bermula dari form HTML biasa: elemen form dengan atribut method dan action. Tanpa JavaScript, browser mengirim data sebagai POST dan memuat ulang halaman — ini adalah baseline yang harus selalu berfungsi.
<form method="POST" action="?/daftar">
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="password">Password</label>
<input id="password" name="password" type="password" required minlength="8" />
<button type="submit">Daftar</button>
</form>Atribut action="?/daftar" menargetkan server action bernama daftar di +page.server.js yang sejalan. Elemen input wajib memiliki atribut name, karena nama itulah kunci data di server.
Server action menerima request dan membaca data lewat request.formData(). Setiap nilai dibaca sebagai string; angka harus dikonversi manual.
Validasi utama harus selalu di server — validasi client hanya kenyamanan, bukan keamanan. Gunakan helper fail dari @sveltejs/kit untuk mengembalikan nilai form beserta pesan error dengan status 4xx.
import { fail } from "@sveltejs/kit";
export const actions = {
daftar: async ({ request }) => {
const form = await request.formData();
const nama = String(form.get("nama") ?? "");
const email = String(form.get("email") ?? "");
if (nama.length < 3) {
return fail(400, { nama, email, error: "Nama minimal 3 karakter" });
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return fail(400, { nama, email, error: "Format email tidak valid" });
}
await simpanPengguna({ nama, email });
return { sukses: true };
}
};Nilai yang dikembalikan dari action menjadi prop form di komponen. Dengan mengembalikan kembali input yang salah, kalian bisa mengisi ulang field tanpa meminta pengguna mengetik ulang.
<script>
import { enhance } from "$app/forms";
let { form } = $props();
</script>
<form method="POST" action="?/daftar" use:enhance>
<label for="nama">Nama</label>
<input id="nama" name="nama" value={form?.nama ?? ""} />
{#if form?.error}
<p class="error" role="alert">{form.error}</p>
{/if}
<button type="submit">Daftar</button>
</form>Prop form tersedia setelah action pertama kali dijalankan. Perhatikan penggunaan role="alert" untuk accessibility: pembaca layar mengumumkan error tanpa memindahkan fokus secara paksa.
Directive use:enhance mengubah submit native menjadi fetch. Hasilnya: halaman tidak reload, form tidak kehilangan fokus, dan pengembang bisa menambahkan transisi atau update parsial.
<script>
import { enhance } from "$app/forms";
let { form } = $props();
let mengirim = $state(false);
</script>
<form method="POST" action="?/daftar"
use:enhance={() => {
mengirim = true;
return async ({ update }) => {
await update();
mengirim = false;
};
}}>
<button type="submit" disabled={mengirim}>
{mengirim ? "Mengirim..." : "Daftar"}
</button>
</form>Fungsi yang dikembalikan dari callback use:enhance menerima object berisi update, result, dan form. Panggil update() untuk menerapkan hasil action ke halaman. Jika JavaScript gagal dimuat, form tetap berjalan sebagai POST native — tidak ada data yang hilang.
Selalu uji form dalam kondisi no-JS, misalnya dengan mematikan JavaScript di devtools atau lewat test E2E seperti npx playwright test. Ini menjamin jalur paling dasar tetap bekerja dan menjadi safety net untuk pengguna dengan koneksi lambat atau browser yang membatasi skrip.
Upload file bekerja lewat form dengan enctype="multipart/form-data". Di server, nilai file muncul sebagai objek File di dalam FormData.
export const actions = {
unggah: async ({ request }) => {
const form = await request.formData();
const file = form.get("gambar");
if (!(file instanceof File) || file.size === 0) {
return fail(400, { error: "Pilih file terlebih dahulu" });
}
if (file.size > 5 * 1024 * 1024) {
return fail(400, { error: "Ukuran file maksimal 5 MB" });
}
const bytes = new Uint8Array(await file.arrayBuffer());
await simpanKeStorage(file.name, file.type, bytes);
return { sukses: true };
}
};Selalu batasi ukuran dan tipe file di server, bukan hanya di client. Untuk file besar, pertimbangkan menyimpan langsung ke object storage (misalnya S3 atau R2) dan simpan hanya metadata-nya di database.
Serverless dan edge runtime sering punya batas ukuran request. Periksa batas provider kalian dan beri pesan yang jelas saat upload melewati limit. Untuk file yang sangat besar, strategi yang tepat adalah upload langsung ke storage lewat presigned URL, di mana server hanya menandatangani izin tanpa memindahkan byte-nya.
Inti yang harus dibawa pulang:
fail mengembalikan nilai dan error dengan status 4xx.form membawa hasil action ke komponen untuk umpan balik UI.use:enhance menambah lapisan JS tanpa menghilangkan fallback native.Di episode 12 selanjutnya kita masuk ke authentication & authorization: auth patterns di SvelteKit, session handling dengan cookies dan server-side auth, protected routes dan authorization guards, serta external auth providers seperti OAuth dan OIDC.