Skip to main content
A +page.server.js file can export actions, which allow you to POST data to the server using the <form> element. When using <form>, client-side JavaScript is optional, but you can easily progressively enhance your form interactions with JavaScript to provide the best user experience.

Default actions

In the simplest case, a page declares a default action:
/// file: src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
	default: async (event) => {
		// TODO log the user in
	}
};
Actions always use POST requests, since GET requests should never have side-effects.

Invoking from other pages

You can invoke the action from other pages by adding the action attribute:
/// file: src/routes/+layout.svelte
<form method="POST" action="/login">
	<!-- content -->
</form>

Named actions

Instead of one default action, a page can have multiple named actions:
/// file: src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
	login: async (event) => {
		// TODO log the user in
	},
	register: async (event) => {
		// TODO register the user
	}
};
To invoke a named action, add a query parameter with the name prefixed by a / character:
<!--- file: src/routes/login/+page.svelte --->
<form method="POST" action="?/register">

Using formaction

You can use the formaction attribute on a button to POST to a different action:
/// file: src/routes/login/+page.svelte
<form method="POST" action="?/login">
	<label>
		Email
		<input name="email" type="email">
	</label>
	<label>
		Password
		<input name="password" type="password">
	</label>
	<button>Log in</button>
	<button formaction="?/register">Register</button>
</form>

Anatomy of an action

Each action receives a RequestEvent object, allowing you to read the data with request.formData():
/// file: src/routes/login/+page.server.js
import * as db from '$lib/server/db';

/** @type {import('./$types').PageServerLoad} */
export async function load({ cookies }) {
	const user = await db.getUserFromSession(cookies.get('sessionid'));
	return { user };
}

/** @satisfies {import('./$types').Actions} */
export const actions = {
	login: async ({ cookies, request }) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');

		const user = await db.getUser(email);
		cookies.set('sessionid', await db.createSession(user), { path: '/' });

		return { success: true };
	},
	register: async (event) => {
		// TODO register the user
	}
};
The returned data is available through the form property:
<!--- file: src/routes/login/+page.svelte --->
<script>
	/** @type {import('./$types').PageProps} */
	let { data, form } = $props();
</script>

{#if form?.success}
	<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}

Validation errors

If the request couldn’t be processed because of invalid data, return validation errors using the fail function:
/// file: src/routes/login/+page.server.js
import { fail } from '@sveltejs/kit';
import * as db from '$lib/server/db';

/** @satisfies {import('./$types').Actions} */
export const actions = {
	login: async ({ cookies, request }) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');

		if (!email) {
			return fail(400, { email, missing: true });
		}

		const user = await db.getUser(email);

		if (!user || user.password !== db.hash(password)) {
			return fail(400, { email, incorrect: true });
		}

		cookies.set('sessionid', await db.createSession(user), { path: '/' });

		return { success: true };
	}
};
Display validation errors in your component:
/// file: src/routes/login/+page.svelte
<form method="POST" action="?/login">
	{#if form?.missing}<p class="error">The email field is required</p>{/if}
	{#if form?.incorrect}<p class="error">Invalid credentials!</p>{/if}
	<label>
		Email
		<input name="email" type="email" value={form?.email ?? ''}>
	</label>
	<label>
		Password
		<input name="password" type="password">
	</label>
	<button>Log in</button>
</form>
As a precaution, we only return the email back to the page — not the password.

Redirects

Redirects work exactly the same as in load functions:
/// file: src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
import * as db from '$lib/server/db';

/** @satisfies {import('./$types').Actions} */
export const actions = {
	login: async ({ cookies, request, url }) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');

		const user = await db.getUser(email);
		if (!user) {
			return fail(400, { email, missing: true });
		}

		if (user.password !== db.hash(password)) {
			return fail(400, { email, incorrect: true });
		}

		cookies.set('sessionid', await db.createSession(user), { path: '/' });

		if (url.searchParams.has('redirectTo')) {
			redirect(303, url.searchParams.get('redirectTo'));
		}

		return { success: true };
	}
};

Loading data

After an action runs, the page will be re-rendered (unless a redirect or an unexpected error occurs):
1

Action completes

The action’s return value is available as the form prop
2

Load functions rerun

Your page’s load functions will run after the action completes
3

Page updates

The page is re-rendered with the new data
Note that handle runs before the action is invoked, and does not rerun before the load functions. If you use handle to populate event.locals, you must update it in the action.

Progressive enhancement

The forms we’ve built work without client-side JavaScript. When JavaScript is available, we can progressively enhance the user experience.

use:enhance

The easiest way to progressively enhance a form is to add the use:enhance action:
/// file: src/routes/login/+page.svelte
<script>
	import { enhance } from '$app/forms';

	/** @type {import('./$types').PageProps} */
	let { form } = $props();
</script>

<form method="POST" use:enhance>
use:enhance can only be used with forms that have method="POST" and point to actions defined in a +page.server.js file.
Without an argument, use:enhance will emulate the browser-native behavior, but without full-page reloads:
Updates the form property and page.form on a successful or invalid response (if the action is on the same page)
Resets the <form> element after successful submission
Invalidates all data using invalidateAll on a successful response
Calls goto on a redirect response
Renders the nearest +error boundary if an error occurs
Resets focus to the appropriate element

Customizing use:enhance

To customize the behavior, provide a SubmitFunction:
<form
	method="POST"
	use:enhance={({ formElement, formData, action, cancel, submitter }) => {
		// `formElement` is this `<form>` element
		// `formData` is its `FormData` object that's about to be submitted
		// `action` is the URL to which the form is posted
		// calling `cancel()` will prevent the submission
		// `submitter` is the `HTMLElement` that caused the form to be submitted

		return async ({ result, update }) => {
			// `result` is an `ActionResult` object
			// `update` triggers the default logic
		};
	}}
>

Using applyAction

To get the default behavior back after customization, use applyAction:
/// file: src/routes/login/+page.svelte
<script>
	import { enhance, applyAction } from '$app/forms';

	/** @type {import('./$types').PageProps} */
	let { form } = $props();
</script>

<form
	method="POST"
	use:enhance={({ formElement, formData, action, cancel }) => {
		return async ({ result }) => {
			if (result.type === 'redirect') {
				goto(result.location);
			} else {
				await applyAction(result);
			}
		};
	}}
>
The behavior of applyAction(result) depends on result.type:
Sets page.status to result.status and updates form and page.form to result.data

Custom event listener

You can also implement progressive enhancement with a normal event listener:
<!--- file: src/routes/login/+page.svelte --->
<script>
	import { invalidateAll, goto } from '$app/navigation';
	import { applyAction, deserialize } from '$app/forms';

	/** @type {import('./$types').PageProps} */
	let { form } = $props();

	/** @param {SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}} event */
	async function handleSubmit(event) {
		event.preventDefault();
		const data = new FormData(event.currentTarget, event.submitter);

		const response = await fetch(event.currentTarget.action, {
			method: 'POST',
			body: data
		});

		/** @type {import('@sveltejs/kit').ActionResult} */
		const result = deserialize(await response.text());

		if (result.type === 'success') {
			await invalidateAll();
		}

		applyAction(result);
	}
</script>

<form method="POST" onsubmit={handleSubmit}>
	<!-- content -->
</form>
You need to deserialize the response before processing it. JSON.parse() isn’t enough because form actions support returning Date or BigInt objects.

Alternatives

Form actions are the preferred way to send data to the server, but you can also use +server.js files to expose a JSON API:
<!--- file: src/routes/send-message/+page.svelte --->
<script>
	function rerun() {
		fetch('/api/ci', {
			method: 'POST'
		});
	}
</script>

<button onclick={rerun}>Rerun CI</button>

GET vs POST

As we’ve seen, to invoke a form action you must use method="POST". Some forms don’t need to POST data to the server — search inputs, for example. For these you can use method="GET":
<form action="/search">
	<label>
		Search
		<input name="q">
	</label>
</form>
Submitting this form will navigate to /search?q=... and invoke your load function:
As with <a> elements, you can set the data-sveltekit-reload, data-sveltekit-replacestate, data-sveltekit-keepfocus and data-sveltekit-noscroll attributes on the <form> to control the router’s behavior.

Next steps