Form Actions
Handling form submissions on the server with +page.server.js actions, without writing a separate API endpoint.
2 min de lectura
A common instinct coming from single-page apps is to handle every form with fetch() in an onsubmit handler, POSTing to a hand-written API route. SvelteKit offers something more direct for forms tied to a specific page: actions, defined right next to the load function they share a route with.
A basic action
// src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email || !password) {
return fail(400, { email, missing: true });
}
const user = await authenticate(email, password);
if (!user) {
return fail(401, { email, invalid: true });
}
cookies.set('session', user.sessionToken, { path: '/' });
redirect(303, '/dashboard');
}
};<!-- src/routes/login/+page.svelte -->
<form method="POST">
<input name="email" type="email" />
<input name="password" type="password" />
<button>Log in</button>
</form>That's the entire feature — no client-side JavaScript required for it to work. method="POST" on a plain <form> is enough for SvelteKit to route the submission to the default action in the co-located +page.server.js. This works even with JavaScript disabled, because it's just a standard HTML form POST under the hood.
Why actions instead of a fetch call
Because the action lives in +page.server.js, it has full access to server-only things — cookies, locals, a database client — without you standing up a separate /api/login route, defining a fetch call, and manually handling the response. The form submission is the request; SvelteKit just gives the handler a good home.
Named actions
A page can expose more than one action when a single page has more than one form:
// src/routes/todos/[id]/+page.server.js
export const actions = {
update: async ({ request }) => { /* ... */ },
delete: async ({ request }) => { /* ... */ }
};<form method="POST" action="?/update">...</form>
<form method="POST" action="?/delete">...</form>The ?/actionName query parameter tells SvelteKit which named action to run — default is only used when a form doesn't specify one.
Returning validation errors
fail() sends data back to the same page without a redirect, so you can re-render the form with the user's input and an error message intact:
<script>
let { form } = $props();
</script>
<form method="POST">
<input name="email" value={form?.email ?? ''} />
{#if form?.missing}
<p class="error">Email and password are required.</p>
{/if}
<button>Log in</button>
</form>form is a prop populated automatically from the last action's return value on this page — distinct from data, which comes from load. Losing sight of that distinction is a common source of confusion: data describes the page, form describes the result of the last form submission on it.
The next lesson layers JavaScript back on top of this — not to replace the server-rendered form, but to make it feel instant without a full page reload, using use:enhance.