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:
Server action
Form component
/// 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" >
<!--- file: src/routes/+layout.svelte --->
< form method = "POST" action = "/login?/register" >
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):
Action completes
The action’s return value is available as the form prop
Load functions rerun
Your page’s load functions will run after the action completes
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:
success/failure
redirect
error
Sets page.status to result.status and updates form and page.form to result.data
Calls goto(result.location, { invalidateAll: true })
Renders the nearest +error boundary with result.error
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