React
SvelteKit form actions: validation and progressive enhancement
A SvelteKit form action validates on the server and returns fail() without reloading the page. Here is where the logic belongs.
Key points
- A form action lives in +page.server.ts and only accepts POST from the same origin.
- fail(400, data) returns the values so the form can be shown again without a reload.
- redirect(303) after a POST stops a refresh from submitting twice.
A SvelteKit form action is a function exported from +page.server.ts: it receives the request, validates the data and returns fail() or throws redirect(). The form works without JavaScript too, because it stays a plain POST.
The smallest action
import { fail, redirect } from "@sveltejs/kit";
import type { Actions } from "./$types";
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = String(data.get("email") ?? "").trim();
if (!email.includes("@")) {
return fail(400, { email, message: "Email non valida" });
}
await subscribe(email);
throw redirect(303, "/grazie");
},
} satisfies Actions;
Status 303 after a POST stops a refresh from submitting again. redirect(307) keeps the method, so a refresh becomes a second POST and the subscription arrives twice.
Showing the error without losing the data
import { enhance } from "$app/forms";
let { form } = $props();
Iscriviti
{#if form?.message}{form.message}{/if}
With use:enhance the POST stays a POST, but the page does not reload. Remove the directive and the form still works: that is what makes the enhancement progressive rather than a requirement.
Where validation belongs
| Layer | What it checks | What it cannot check |
|---|---|---|
| Browser input | Immediate format, minimal friction | Trust: it can be bypassed |
| Action | Domain rules and permissions | The state of the interface |
| Database | Constraints and uniqueness | The message to show |
There is one rule: validation that protects data lives in the action. The check in the browser is there for speed, and it replaces nothing.
Common mistakes
fail()without the submitted values: the form is wiped and the reader types everything again.redirect()called withoutthrow: execution continues and the response is sent twice.- A named action and a form pointing at
?/default: the names do not match and the POST answers 404. - Fields whose
nameattribute differs from the key read informData().
The known limitation
Form actions need a server. With adapter-static they do not exist: the form has to call an external endpoint or an API route on another origin, and you lose the server-side session.
Commands
npm create svelte@latest my-app
npx svelte-kit sync
From the catalogue: svelte-bits, flowbite-svelte and the Svelte logo block. The guides: the store that does not update and svelte-bits, measured; the comparison: Svelte against React for content sites.
Frequently asked questions
Why does the form return 405?
A form action only accepts POST from the same origin. A GET link to the page or a fetch without a POST method produces 405.
How do I read the submitted data?
With request.formData() inside the action. The result is a FormData object, so every value needs converting to a string.
What is the form field in the page for?
It holds what the action returns through fail, so you can show the values and the error message without reloading the page.
Why does redirect not work?
redirect must be thrown, or returned as a value. Call it and ignore it, and execution continues without redirecting the response.
Does it work with several forms on one page?
Yes, with named actions: each form points at a name after the slash and the action exports the key with the same name.