Form action adalah function di +page.server.js yang menerima isi sebuah <form> saat form itu dikirim dengan method="POST". Bayangin kertas pesanan di kasir Warung Bu Sri. Pembeli menulis nama, menu, dan jumlah porsi, lalu menyerahkannya ke kasir. Kalau namanya kosong, kasir mengembalikan kertasnya dengan catatan merah, dan tulisan yang sudah ada tetap di kertas itu. Kalau beres, kasir menyuruh pembeli pindah ke meja tunggu. Kertas itu <form>, kasir itu action di server, kertas yang dikembalikan itu fail(400, ...), dan pindah meja itu redirect(303, ...).
🧪 Tebakan cepat: JavaScript di browser pembeli mati, atau belum selesai dimuat. Dia mengisi form pesanan lalu menekan Pesan. Pesanannya tetap sampai ke server, atau tombolnya diam saja? Jawabannya ada di bagian "Tanpa JavaScript pun jalan".
Action di +page.server.js
Halaman /pesan Warung Bu Sri punya dua file. Ini yang di server:
// src/routes/pesan/+page.server.js
import { fail, redirect } from "@sveltejs/kit"
import { semuaMenu } from "#lib/menu.js"
export const actions = {
default: async ({ request }) => {
const data = await request.formData()
const nama = String(data.get("nama") ?? "").trim()
const menuId = Number(data.get("menu"))
const porsi = Number(data.get("porsi"))
if (!nama) {
return fail(400, { nama, menuId, porsi, pesan: "Nama pemesan wajib diisi" })
}
if (porsi < 1) {
return fail(400, { nama, menuId, porsi, pesan: "Porsi minimal 1" })
}
const menu = semuaMenu.find((m) => m.id === menuId)
console.log("pesanan masuk:", nama, menu.nama, porsi, "porsi")
redirect(303, "/pesan/terima-kasih")
},
}
export const actionsadalah objek berisi action. Kuncidefaultmenerima form yang dikirim ke halaman ini sendiri, yaitu<form method="POST">tanpa atributaction.request.formData()membaca isi form.data.get("nama")mengambil isi kolom yang punyaname="nama". Semua isinya datang sebagai string, jadi"3"diubah dulu denganNumber().fail(400, {...})menolak kiriman dengan status 400, dan objeknya sampai di halaman sebagaiform.redirect(303, "/pesan/terima-kasih")menyuruh browser membuka alamat lain. Sepertierror(), dia melempar dirinya sendiri, jadi gak perlureturnatauthrow.- Action cuma boleh di
+page.server.js. Ditaruh di+page.js, SvelteKit berhenti denganInvalid export `actions` in `src/routes/pesan/+page.js` (`actions` is a valid export in +page.server.js).
Halamannya: export let form
<!-- src/routes/pesan/+page.svelte -->
<script>
import { semuaMenu } from "#lib/menu.js"
export let form
</script>
<h2>Pesan</h2>
{#if form?.pesan}
<p class="error">{form.pesan}</p>
{/if}
<form method="POST">
<label>Nama <input name="nama" value={form?.nama ?? ""} /></label>
<label>
Menu
<select name="menu">
{#each semuaMenu as m (m.id)}
<option value={m.id} selected={form?.menuId === m.id}>{m.nama}</option>
{/each}
</select>
</label>
<label>Porsi <input name="porsi" type="number" value={form?.porsi ?? 1} /></label>
<button>Pesan</button>
</form>
export let formmenerima hasil action terakhir. Saat halaman dibuka biasa,formberisinull, makanya ditulisform?.pesan.- Tiap kolom punya
name. Kolom tanpanamegak ikut terkirim, dandata.get(...)untuknya berisinull. value={form?.porsi ?? 1}mengisi kolom dari kiriman yang ditolak, atau1kalau belum ada.
Hasilnya, dicoba di SvelteKit 3 dengan JavaScript dimatikan:
| Isian | Request | Jawaban server | Yang tampil |
|---|---|---|---|
| nama kosong, Mie Ayam, 3 porsi | POST /pesan, isi nama=&menu=2&porsi=3 |
400, halaman /pesan digambar ulang |
"Nama pemesan wajib diisi", Mie Ayam dan 3 masih terisi |
| Andi, Mie Ayam, 3 porsi | POST /pesan, isi nama=Andi&menu=2&porsi=3 |
303, location: /pesan/terima-kasih |
browser membuka GET /pesan/terima-kasih: "Terima kasih!" |
Terminal server mencatat pesanan masuk: Andi Mie Ayam 3 porsi.
Kembalikan isiannya lewat fail
Tanpa JavaScript, jawaban 400 adalah halaman baru. Browser lupa semua yang tadi diketik, dan isi kolom datang dari value={form?....}. Saat fail cuma mengembalikan { pesan }, kiriman "nama kosong, Mie Ayam, 3 porsi" kembali sebagai Nasi Goreng dan 1 porsi; pembeli harus mengisi ulang semuanya. Dengan use:enhance (di bawah) pun, Nama dan Porsi kembali ke nilai bawaannya begitu form berubah. Karena itu kirim balik isiannya: fail(400, { nama, menuId, porsi, pesan }).
Pengecualiannya password: jangan pernah dikirim balik. Untuk form login, kembalikan email saja.
Tanpa JavaScript pun jalan
<form method="POST"> adalah HTML biasa. Browser sudah tahu cara mengirimnya tanpa JavaScript, dan server menjawab dengan halaman utuh. Jadi pesanannya tetap sampai. Itu jawaban tebakannya. Cara membangun seperti ini disebut progressive enhancement: fiturnya jalan dulu tanpa JavaScript, lalu jadi lebih nyaman kalau JavaScript ada. Inilah kelebihan form action dibanding API endpoint yang dipanggil dengan fetch: tanpa JavaScript, fetch gak jalan sama sekali.
Lebih nyamannya datang dari use:enhance, sebuah action Svelte (pelajaran Actions) dari $app/forms:
<script>
import { enhance } from "$app/forms"
import { semuaMenu } from "#lib/menu.js"
export let form
</script>
<form method="POST" use:enhance>
<!-- kolom yang sama -->
</form>
Dengan JavaScript, use:enhance mencegat pengirimannya dan mengirim POST /pesan lewat fetch, dengan header x-sveltekit-action: true. Server menjawab JSON, bukan halaman:
400 {"type":"failure","status":400,"location":"/pesan","data":"[{\"nama\":1,\"menuId\":2,\"porsi\":3,\"pesan\":4},\"\",2,3,\"Nama pemesan wajib diisi\"]"}
200 {"type":"redirect","status":303,"location":"/pesan/terima-kasih"}
- Gagal: halaman gak dimuat ulang.
formdiperbarui dan pesan merah muncul. - Berhasil: SvelteKit pindah ke
/pesan/terima-kasihdi browser, tanpa memuat ulang header-nya. - Tanpa JavaScript, form yang sama tetap jalan seperti tabel di atas.
Beberapa tombol: named action
Satu halaman bisa punya beberapa action. Beri nama di objek actions, misalnya pesan: async ({ request }) => {...} dan batal: async ({ request }) => {...}, lalu tunjuk lewat atribut action:
<form method="POST" action="?/pesan">...</form>
<form method="POST" action="?/batal"><button>Batal</button></form>
default gak boleh dicampur dengan action bernama; SvelteKit menjawab 500 dan terminal menulis When using named actions, the default action cannot be used. Form tanpa ?/... ke halaman yang cuma punya action bernama mendapat 404.
Proyek baru: SvelteKit 3 dan Svelte 5
Di proyek dari npx sv create, runes menyala, jadi halaman menulis let { form } = $props(); export let form ditolak dengan Cannot use export let in runes mode. Sisanya sama. Editor latihan meng-compile dengan Svelte 4, jadi latihannya memakai export let form.
Latihan: form login
Editor latihan menjalankan beberapa file route sekaligus, seperti SvelteKit kecil: src/routes/login/+page.svelte, src/routes/login/+page.server.js, dan src/routes/dashboard/+page.svelte. Form POST mengirim isiannya ke action di +page.server.js sebagai Request sungguhan, jadi await request.formData() jalan. Server-nya gak ada sungguhan: action-nya jalan di pratinjau.
// src/routes/login/+page.server.js
import { fail, redirect } from "@sveltejs/kit"
export const actions = {
default: async ({ request }) => {
const data = await request.formData()
const email = data.get("email")
const password = data.get("password")
if (password !== "rahasia123") {
return fail(400, { email, error: "Password salah" })
}
redirect(303, "/dashboard")
},
}
Dengan password yang salah, fail mengirim objeknya ke halaman sebagai form: pesan Password salah tampil dan email yang sudah diketik tetap ada lewat value={form?.email ?? ""}. Dengan rahasia123, redirect membawa ke /dashboard.
⚠️ Jebakan yang sering ketemu
- Lupa
method="POST". Form tanpa method dikirim sebagai GET: browser membuka/pesan?nama=Andi&menu=2&porsi=3, halamannya tampil biasa, dan action gak pernah jalan. - Kolom tanpa
name. Isinya gak ikut terkirim, jadidata.get("password")jadinulldan login-nya selalu gagal. failtanpa isian. Tanpa JavaScript, pembeli harus mengetik ulang semuanya. Kirim balik isiannya, kecuali password.- Sukses tanpa
redirect. Kalau action cumareturn { sukses: true }, halaman hasilnya adalah jawaban POST. Pembeli menekan refresh, browser menawarkan mengirim ulang form, dan pesanan masuk dua kali.redirect(303, ...)memindahkannya ke halaman GET biasa.
TL;DR: Form action ditulis di +page.server.js sebagai export const actions = { default: async ({ request }) => {...} } dan menerima <form method="POST">. Isinya dibaca dengan await request.formData() dan data.get("nama"), semuanya string, dan cuma kolom ber-name yang terkirim. fail(400, { ...isian, pesan }) menolak kiriman dan mengirim objeknya ke halaman sebagai form (export let form, atau let { form } = $props() di proyek runes); kembalikan isiannya supaya gak perlu diketik ulang, kecuali password. redirect(303, "/pesan/terima-kasih") setelah sukses mencegah kiriman ganda saat refresh. Form-nya HTML biasa, jadi tetap jalan tanpa JavaScript: itu progressive enhancement. use:enhance dari $app/forms menambah pengiriman lewat fetch tanpa memuat ulang halaman. Beberapa action diberi nama dan ditunjuk dengan action="?/batal", dan gak boleh dicampur dengan default. Latihannya: form login dengan <input name="email"> dan <input name="password">, dan action yang mengembalikan fail(400, { email, error: "Password salah" }) atau redirect(303, "/dashboard").