SvelteKit • Core concepts
Form actions
On this page
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.
The experimental
formremote function covers the same use cases as form actions, adding type safety and single-flight mutations. Form actions are feature-complete and will continue to work, but new development is focused on remote functions, which are intended to become the recommended way to communicate with the server. Consider remote functions for new projects, keeping in mind that the API may change while the feature is experimental.
Default actions
In the simplest case, a page declares a default action:
import type { type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions } from './$types';
export const const actions: {
default: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
const actions: {
default: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
default: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>default: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>default: async (event: RequestEvent<Record<string, any>, string | null>event: RequestEvent<Record<string, any>, string | null>event) => {
// TODO log the user in
}
} satisfies type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions;/** @satisfies {import('./$types').Actions} */
export const const actions: {
default: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
const actions: {
default: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
default: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>default: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>default: async (event: RequestEvent<Record<string, any>, string | null>event: RequestEvent<Record<string, any>, string | null>event) => {
// TODO log the user in
}
};To invoke this action from the /login page, just add a <form> — no JavaScript needed:
<form method="POST">
<label>
Email
<input name="email" type="email">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
</form>If someone were to click the button, the browser would send the form data via POST request to the server, running the default action.
Actions always use
POSTrequests, sinceGETrequests should never have side-effects.
We can also invoke the action from other pages (for example if there’s a login widget in the nav in the root layout) by adding the action attribute, pointing to the page:
<form method="POST" action="/login">
<!-- content -->
</form>Named actions
Instead of one default action, a page can have as many named actions as it needs:
import type { Actions } from './$types';
export const actions = { satisfies Actions
default: async (event) => {
login: async (event) => {
// TODO log the user in
},
register: async (event) => {
// TODO register the user
}
};/** @satisfies {import('./$types').Actions} */
export const const actions: {
login: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
const actions: {
login: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
default: async (event) => {
login: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>login: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>login: async (event: RequestEvent<Record<string, any>, string | null>event: RequestEvent<Record<string, any>, string | null>event) => {
// TODO log the user in
},
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>register: async (event: RequestEvent<Record<string, any>, string | null>event: RequestEvent<Record<string, any>, string | null>event) => {
// TODO register the user
}
};To invoke a named action, add a query parameter with the name prefixed by a / character:
<form method="POST" action="?/register"><form method="POST" action="/login?/register">As well as the action attribute, we can use the formaction attribute on a button to POST the same form data to a different action than the parent <form>:
<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>We can’t have default actions next to named actions, because if you POST to a named action without a redirect, the query parameter is persisted in the URL, which means the next default POST would go through the named action from before.
Anatomy of an action
Each action receives a RequestEvent object, allowing you to read the data with request.formData(). After processing the request (for example, logging the user in by setting a cookie), the action can respond with data that will be available through the form property on the corresponding page and through page.form app-wide until the next update.
// @filename: ambient.d.ts
declare module '$lib/server/db';
// @filename: index.ts
// cut
import * as module "$lib/server/db"db from '$lib/server/db';
import type { type PageServerLoad = (event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageServerLoad, type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions } from './$types';
export const const load: PageServerLoadload: type PageServerLoad = (event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageServerLoad = async ({ cookies: CookiesGet or set cookies related to the current request
cookies }) => {
const const user: anyuser = await module "$lib/server/db"db.getUserFromSession(cookies: CookiesGet or set cookies related to the current request
cookies.Cookies.get: (name: string, opts?: CookieParseOptions) => string | undefinedGets a cookie that was previously set with cookies.set, or from the request headers.
get('sessionid'));
return { user: anyuser };
};
export const const actions: {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<{
success: boolean;
}>;
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<{
success: boolean;
}>
login: async ({ cookies: CookiesGet or set cookies related to the current request
cookies, request: RequestThe original request object.
request }) => {
const const data: FormDatadata = await request: RequestThe original request object.
request.Body.formData(): Promise<FormData>formData();
const const email: FormDataEntryValue | nullemail = const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get('email');
const const password: FormDataEntryValue | nullpassword = const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get('password');
const const user: anyuser = await module "$lib/server/db"db.getUser(const email: FormDataEntryValue | nullemail);
cookies: CookiesGet or set cookies related to the current request
cookies.Cookies.set: (name: string, value: string, opts: CookieSerializeOptions & {
path: string;
}) => void
Sets a cookie. This will add a set-cookie header to the response, but also make the cookie available via cookies.get or cookies.getAll during the current request.
The httpOnly and secure options are true by default (except on http://localhost, where secure is false), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The sameSite option defaults to lax.
You must specify a path for the cookie. In most cases you should explicitly set path: '/' to make the cookie available throughout your app. You can use relative paths, or set path: '' to make the cookie only available on the current path and its children
set('sessionid', await module "$lib/server/db"db.createSession(const user: anyuser), { path: stringSpecifies the value for the
{@link
https://tools.ietf.org/html/rfc6265#section-5.2.4 Path Set-Cookie attribute
}
.
By default, the path is considered the "default path".
path: '/' });
return { success: booleansuccess: true };
},
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>register: async (event: RequestEvent<Record<string, any>, string | null>event) => {
// TODO register the user
}
} satisfies type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions;// @filename: ambient.d.ts
declare module '$lib/server/db';
// @filename: index.js
// cut
import * as module "$lib/server/db"db from '$lib/server/db';
/** @type {import('./$types').PageServerLoad} */
export async function function load(event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>load({ cookies: CookiesGet or set cookies related to the current request
cookies }) {
const const user: anyuser = await module "$lib/server/db"db.getUserFromSession(cookies: CookiesGet or set cookies related to the current request
cookies.Cookies.get: (name: string, opts?: CookieParseOptions) => string | undefinedGets a cookie that was previously set with cookies.set, or from the request headers.
get('sessionid'));
return { user: anyuser };
}
/** @satisfies {import('./$types').Actions} */
export const const actions: {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<{
success: boolean;
}>;
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<{
success: boolean;
}>
login: async ({ cookies: CookiesGet or set cookies related to the current request
cookies, request: RequestThe original request object.
request }) => {
const const data: FormDatadata = await request: RequestThe original request object.
request.Body.formData(): Promise<FormData>formData();
const const email: FormDataEntryValue | nullemail = const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get('email');
const const password: FormDataEntryValue | nullpassword = const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get('password');
const const user: anyuser = await module "$lib/server/db"db.getUser(const email: FormDataEntryValue | nullemail);
cookies: CookiesGet or set cookies related to the current request
cookies.Cookies.set: (name: string, value: string, opts: CookieSerializeOptions & {
path: string;
}) => void
Sets a cookie. This will add a set-cookie header to the response, but also make the cookie available via cookies.get or cookies.getAll during the current request.
The httpOnly and secure options are true by default (except on http://localhost, where secure is false), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The sameSite option defaults to lax.
You must specify a path for the cookie. In most cases you should explicitly set path: '/' to make the cookie available throughout your app. You can use relative paths, or set path: '' to make the cookie only available on the current path and its children
set('sessionid', await module "$lib/server/db"db.createSession(const user: anyuser), { path: stringSpecifies the value for the
{@link
https://tools.ietf.org/html/rfc6265#section-5.2.4 Path Set-Cookie attribute
}
.
By default, the path is considered the "default path".
path: '/' });
return { success: booleansuccess: true };
},
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>register: async (event: RequestEvent<Record<string, any>, string | null>event) => {
// TODO register the user
}
};<script lang="ts">
import type { PageProps } from './$types';
let { data, form }: PageProps = $props();
</script>
{#if form?.success}
<!-- this message is ephemeral; it exists because the page was rendered in
response to a form submission. it will vanish if the user reloads -->
<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}<script>
/** @type {import('./$types').PageProps} */
let { data, form } = $props();
</script>
{#if form?.success}
<!-- this message is ephemeral; it exists because the page was rendered in
response to a form submission. it will vanish if the user reloads -->
<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}Legacy mode
PagePropswas added in 2.16.0. In earlier versions, you had to type thedataandformproperties individually:import type { PageData, ActionData } from './$types'; let { data, form }: { data: PageData, form: ActionData } = $props();/** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */ let { data, form } = $props();In Svelte 4, you’d use
export let dataandexport let forminstead to declare properties.
Validation errors
If the request couldn’t be processed because of invalid data, you can return validation errors — along with the previously submitted form values — back to the user so that they can try again. The fail function lets you return an HTTP status code (typically 400 or 422, in the case of validation errors) along with the data. The status code is available through page.status and the data through form:
// @filename: ambient.d.ts
declare module '$lib/server/db';
// @filename: index.ts
// cut
import { function fail(status: number): ActionFailure<undefined> (+1 overload)Create an ActionFailure object. Call when form submission fails.
fail } from '@sveltejs/kit';
import * as module "$lib/server/db"db from '$lib/server/db';
import type { type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions } from './$types';
export const const actions: {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: string | null;
missing: boolean;
}> | ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> | {
success: boolean;
}>;
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: string | null;
missing: boolean;
}> | ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> | {
success: boolean;
}>
login: async ({ cookies: CookiesGet or set cookies related to the current request
cookies, request: RequestThe original request object.
request }) => {
const const data: FormDatadata = await request: RequestThe original request object.
request.Body.formData(): Promise<FormData>formData();
const const email: FormDataEntryValue | nullemail = const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get('email');
const const password: FormDataEntryValue | nullpassword = const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get('password');
if (!const email: FormDataEntryValue | nullemail) {
return fail<{
email: string | null;
missing: boolean;
}>(status: number, data: {
email: string | null;
missing: boolean;
}): ActionFailure<{
email: string | null;
missing: boolean;
}> (+1 overload)Create an ActionFailure object. Call when form submission fails.
email: string | nullemail, missing: booleanmissing: true });
}
const const user: anyuser = await module "$lib/server/db"db.getUser(const email: FormDataEntryValueemail);
if (!const user: anyuser || const user: anyuser.password !== module "$lib/server/db"db.hash(const password: FormDataEntryValue | nullpassword)) {
return fail<{
email: FormDataEntryValue;
incorrect: boolean;
}>(status: number, data: {
email: FormDataEntryValue;
incorrect: boolean;
}): ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> (+1 overload)Create an ActionFailure object. Call when form submission fails.
email: FormDataEntryValueemail, incorrect: booleanincorrect: true });
}
cookies: CookiesGet or set cookies related to the current request
Cookies.set: (name: string, value: string, opts: CookieSerializeOptions & {
path: string;
}) => void
Sets a cookie. This will add a set-cookie header to the response, but also make the cookie available via cookies.get or cookies.getAll during the current request.
The httpOnly and secure options are true by default (except on http://localhost, where secure is false), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The sameSite option defaults to lax.
You must specify a path for the cookie. In most cases you should explicitly set path: '/' to make the cookie available throughout your app. You can use relative paths, or set path: '' to make the cookie only available on the current path and its children
module "$lib/server/db"db.createSession(const user: anyuser), { path: stringSpecifies the value for the
{@link
https://tools.ietf.org/html/rfc6265#section-5.2.4 Path Set-Cookie attribute
}
.
By default, the path is considered the "default path".
success: booleansuccess: true };
},
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>register: async (event: RequestEvent<Record<string, any>, string | null>event) => {
// TODO register the user
}
} satisfies type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions;// @filename: ambient.d.ts
declare module '$lib/server/db';
// @filename: index.js
// cut
import { function fail(status: number): ActionFailure<undefined> (+1 overload)Create an ActionFailure object. Call when form submission fails.
fail } from '@sveltejs/kit';
import * as module "$lib/server/db"db from '$lib/server/db';
/** @satisfies {import('./$types').Actions} */
export const const actions: {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: string | null;
missing: boolean;
}> | ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> | {
success: boolean;
}>;
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: string | null;
missing: boolean;
}> | ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> | {
success: boolean;
}>
login: async ({ cookies: CookiesGet or set cookies related to the current request
cookies, request: RequestThe original request object.
request }) => {
const const data: FormDatadata = await request: RequestThe original request object.
request.Body.formData(): Promise<FormData>formData();
const const email: FormDataEntryValue | nullemail = const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get('email');
const const password: FormDataEntryValue | nullpassword = const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get('password');
if (!const email: FormDataEntryValue | nullemail) {
return fail<{
email: string | null;
missing: boolean;
}>(status: number, data: {
email: string | null;
missing: boolean;
}): ActionFailure<{
email: string | null;
missing: boolean;
}> (+1 overload)Create an ActionFailure object. Call when form submission fails.
email: string | nullemail, missing: booleanmissing: true });
}
const const user: anyuser = await module "$lib/server/db"db.getUser(const email: FormDataEntryValueemail);
if (!const user: anyuser || const user: anyuser.password !== module "$lib/server/db"db.hash(const password: FormDataEntryValue | nullpassword)) {
return fail<{
email: FormDataEntryValue;
incorrect: boolean;
}>(status: number, data: {
email: FormDataEntryValue;
incorrect: boolean;
}): ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> (+1 overload)Create an ActionFailure object. Call when form submission fails.
email: FormDataEntryValueemail, incorrect: booleanincorrect: true });
}
cookies: CookiesGet or set cookies related to the current request
Cookies.set: (name: string, value: string, opts: CookieSerializeOptions & {
path: string;
}) => void
Sets a cookie. This will add a set-cookie header to the response, but also make the cookie available via cookies.get or cookies.getAll during the current request.
The httpOnly and secure options are true by default (except on http://localhost, where secure is false), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The sameSite option defaults to lax.
You must specify a path for the cookie. In most cases you should explicitly set path: '/' to make the cookie available throughout your app. You can use relative paths, or set path: '' to make the cookie only available on the current path and its children
module "$lib/server/db"db.createSession(const user: anyuser), { path: stringSpecifies the value for the
{@link
https://tools.ietf.org/html/rfc6265#section-5.2.4 Path Set-Cookie attribute
}
.
By default, the path is considered the "default path".
success: booleansuccess: true };
},
register: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>register: async (event: RequestEvent<Record<string, any>, string | null>event) => {
// TODO register the user
}
};Note that as a precaution, we only return the email back to the page — not the password.
<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>
<button formaction="?/register">Register</button>
</form>The returned data must be serializable as JSON. Beyond that, the structure is entirely up to you. For example, if you had multiple forms on the page, you could distinguish which <form> the returned form data referred to with an id property or similar.
Redirects
Redirects (and errors) work exactly the same as in load:
// @errors: 2345
// @filename: ambient.d.ts
declare module '$lib/server/db';
// @filename: index.ts
// cut
import { fail, redirect } from '@sveltejs/kit';
import * as db from '$lib/server/db';
import type { Actions } from './$types';
export const actions = {
login: async ({ cookies, request, url } satisfies Actions) => {
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 };
},
register: async (event) => {
// TODO register the user
}
};// @filename: ambient.d.ts
declare module '$lib/server/db';
// @filename: index.js
// cut
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 };
},
register: async (event) => {
// TODO register the user
}
};Loading data
After an action runs, the page will be re-rendered (unless a redirect or an unexpected error occurs), with the action’s return value available to the page as the form prop. This means that your page’s load functions will run after the action completes.
Note that handle runs before the action is invoked, and does not rerun before the load functions. This means that if, for example, you use handle to populate event.locals based on a cookie, you must update event.locals when you set or delete the cookie in an action:
import type { Handle } from '@sveltejs/kit';
// @filename: ambient.d.ts
declare namespace App {
interface Locals {
user: {
name: string;
} | null
}
}
// @filename: global.d.ts
declare global {
function getUser(sessionid: string | undefined): {
name: string;
};
}
export {};
// @filename: index.ts
// cut
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await getUser(event.cookies.get('sessionid'));
return resolve(event);
};// @filename: ambient.d.ts
declare namespace App {
interface interface App.LocalsThe interface that defines event.locals, which can be accessed in server hooks (handle, and handleError), server-only load functions, and +server.js files.
interface App.LocalsThe interface that defines event.locals, which can be accessed in server hooks (handle, and handleError), server-only load functions, and +server.js files.
Locals {
App.Locals.user: {
name: string;
} | null
App.Locals.user: {
name: string;
} | null
user: {
name: stringname: stringname: string;
} | null
}
}
// @filename: global.d.ts
declare global {
function function getUser(sessionid: string | undefined): {
name: string;
}
function getUser(sessionid: string | undefined): {
name: string;
}
getUser(sessionid: string | undefinedsessionid: string | undefinedsessionid: string | undefined): {
name: stringname: stringname: string;
};
}
export {};
// @filename: index.js
// cut
/** @type {import('@sveltejs/kit').Handle} */
export async function function handle(input: {
event: RequestEvent;
resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>;
}): MaybePromise<Response>
handle({ event: RequestEvent<Record<string, string>, string | null>event, resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>resolve }) {
event: RequestEvent<Record<string, string>, string | null>event.RequestEvent<Record<string, string>, string | null>.locals: App.LocalsContains custom data that was added to the request within the server handle hook.
locals.App.Locals.user: {
name: string;
} | null
user = await function getUser(sessionid: string | undefined): {
name: string;
}
getUser(event: RequestEvent<Record<string, string>, string | null>event.RequestEvent<Record<string, string>, string | null>.cookies: CookiesGet or set cookies related to the current request
cookies.Cookies.get: (name: string, opts?: CookieParseOptions) => string | undefinedGets a cookie that was previously set with cookies.set, or from the request headers.
get('sessionid'));
return resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>resolve(event: RequestEvent<Record<string, string>, string | null>event);
}import type { PageServerLoad, Actions } from './$types';
// @filename: ambient.d.ts
declare namespace App {
interface Locals {
user: {
name: string;
} | null
}
}
// @filename: index.ts
// cut
export const load: PageServerLoad = (event) => {
return {
user: event.locals.user
};
};
export const actions = {
logout: async (event) => {
event.cookies.delete('sessionid', { path: '/' });
event.locals.user = null;
}
} satisfies Actions;// @filename: ambient.d.ts
declare namespace App {
interface interface App.LocalsThe interface that defines event.locals, which can be accessed in server hooks (handle, and handleError), server-only load functions, and +server.js files.
interface App.LocalsThe interface that defines event.locals, which can be accessed in server hooks (handle, and handleError), server-only load functions, and +server.js files.
Locals {
App.Locals.user: {
name: string;
} | null
App.Locals.user: {
name: string;
} | null
user: {
name: stringname: stringname: string;
} | null
}
}
// @filename: index.js
// cut
/** @type {import('./$types').PageServerLoad} */
export function function load(event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>load(event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>event) {
return {
user: {
name: string;
} | null
user: event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>event.RequestEvent<Record<string, any>, string | null>.locals: App.LocalsContains custom data that was added to the request within the server handle hook.
locals.App.Locals.user: {
name: string;
} | null
user
};
}
/** @satisfies {import('./$types').Actions} */
export const const actions: {
logout: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
logout: (event: RequestEvent<Record<string, any>, string | null>) => Promise<void>logout: async (event: RequestEvent<Record<string, any>, string | null>event) => {
event: RequestEvent<Record<string, any>, string | null>event.RequestEvent<Record<string, any>, string | null>.cookies: CookiesGet or set cookies related to the current request
cookies.Cookies.delete: (name: string, opts: CookieSerializeOptions & {
path: string;
}) => void
Deletes a cookie by setting its value to an empty string and setting the expiry date in the past.
You must specify a path for the cookie. In most cases you should explicitly set path: '/' to make the cookie available throughout your app. You can use relative paths, or set path: '' to make the cookie only available on the current path and its children
delete('sessionid', { path: stringSpecifies the value for the
{@link
https://tools.ietf.org/html/rfc6265#section-5.2.4 Path Set-Cookie attribute
}
.
By default, the path is considered the "default path".
path: '/' });
event: RequestEvent<Record<string, any>, string | null>event.RequestEvent<Params extends LayoutParams<"/"> = Record<string, string>, RouteId extends RouteId | null = string | null>.locals: App.LocalsContains custom data that was added to the request within the server handle hook.
locals.App.Locals.user: {
name: string;
} | null
user = null;
}
};Progressive enhancement
In the preceding sections we built a /login action that works without client-side JavaScript — not a fetch in sight. That’s great, but when JavaScript is available we can progressively enhance our form interactions to provide a better user experience.
use:enhance
The easiest way to progressively enhance a form is to add the use:enhance action:
<script lang="ts">
import { enhance } from '$app/forms';
/** @type {import('./$types').PageProps} */
let { form } = $props();
</script>
<form method="POST" use:enhance><script>
import { enhance } from '$app/forms';
/** @type {import('./$types').PageProps} */
let { form } = $props();
</script>
<form method="POST" use:enhance>
use:enhancecan only be used with forms that havemethod="POST"and point to actions defined in a+page.server.jsfile. It will not work withmethod="GET", which is the default for forms without a specified method. Attempting to useuse:enhanceon forms withoutmethod="POST"or posting to a+server.jsendpoint will result in an error.
Yes, it’s a little confusing that the
enhanceaction and<form action>are both called ‘action’. These docs are action-packed. Sorry.
Without an argument, use:enhance will emulate the browser-native behaviour, just without the full-page reloads. It will:
- update the
formproperty,page.formandpage.statuson a successful or invalid response, but only if the action is on the same page you’re submitting from. For example, if your form looks like<form action="/somewhere/else" ..>, theformprop and thepage.formstate will not be updated. This is because in the native form submission case you would be redirected to the page the action is on. If you want to have them updated either way, useapplyAction - reset the
<form>element - invalidate all data using
invalidateAllon a successful response - call
gotoon a redirect response - render the nearest
+errorboundary if an error occurs - reset focus to the appropriate element
Customising use:enhance
To customise the behaviour, you can provide a SubmitFunction that runs immediately before the form is submitted, and (optionally) returns a callback that runs with the ActionResult.
<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` is a function which triggers the default logic that would be triggered if this callback wasn't set
};
}}
>You can use these functions to show and hide loading UI, and so on.
If you return a callback, you override the default post-submission behavior. To get it back, call update, which accepts invalidateAll and reset parameters, or use applyAction on the result:
<script lang="ts">
import { enhance, applyAction
import type { PageProps } from './$types'; } from '$app/forms';
let { form }: PageProps = $props();
</script>
<form
method="POST"
use:enhance={({ formElement, formData, action, cancel }) => {
return async ({ result }) => {
// `result` is an `ActionResult` object
if (result.type === 'redirect') {
goto(result.location);
} else {
await applyAction(result);
}
};
}}
><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 }) => {
// `result` is an `ActionResult` object
if (result.type === 'redirect') {
goto(result.location);
} else {
await applyAction(result);
}
};
}}
>The behaviour of applyAction(result) depends on result.type:
success,failure— setspage.statustoresult.statusand updatesformandpage.formtoresult.data(regardless of where you are submitting from, in contrast toupdatefromenhance)redirect— callsgoto(result.location, { invalidateAll: true })error— renders the nearest+errorboundary withresult.error
In all cases, focus will be reset.
Custom event listener
We can also implement progressive enhancement ourselves, without use:enhance, with a normal event listener on the <form>:
<script lang="ts">
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';
import type { PageProps } from './$types';
import type { ActionResult } from '@sveltejs/kit';
let { form }: PageProps = $props();
async function handleSubmit(event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}) {
event.preventDefault();
const data = new FormData(event.currentTarget, event.submitter);
const response = await fetch(event.currentTarget.action, {
method: 'POST',
body: data
});
const result: ActionResult = deserialize(await response.text());
if (result.type === 'success') {
// rerun all `load` functions, following the successful update
await invalidateAll();
}
applyAction(result);
}
</script>
<form method="POST" onsubmit={handleSubmit}>
<!-- content -->
</form><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') {
// rerun all `load` functions, following the successful update
await invalidateAll();
}
applyAction(result);
}
</script>
<form method="POST" onsubmit={handleSubmit}>
<!-- content -->
</form>Note that you need to deserialize the response before processing it further using the corresponding method from $app/forms. JSON.parse() isn’t enough because form actions - like load functions - also support returning Date or BigInt objects.
If you have a +server.js alongside your +page.server.js, fetch requests will be routed there by default. To POST to an action in +page.server.js instead, use the custom x-sveltekit-action header:
const const response: Responseresponse = await function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)fetch(this.action, { RequestInit.method?: string | undefinedA string to set request's method.
method: 'POST',
RequestInit.body?: BodyInit | null | undefinedA BodyInit object or null to set request's body.
body: data, RequestInit.headers?: HeadersInit | undefinedA Headers object, an object literal, or an array of two-item arrays to set request's headers.
headers: {
'x-sveltekit-action': 'true'
}
});Alternatives
Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use +server.js files to expose (for example) a JSON API. Here’s how such an interaction could look like:
<script lang="ts">
function function (local function) rerun(): voidrerun() {
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)fetch('/api/ci', {
RequestInit.method?: string | undefinedA string to set request's method.
method: 'POST'
});
}
</script>
<button onclick={function (local function) rerun(): voidrerun}>Rerun CI</button><script>
function function (local function) rerun(): voidrerun() {
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)fetch('/api/ci', {
RequestInit.method?: string | undefinedA string to set request's method.
method: 'POST'
});
}
</script>
<button onclick={function (local function) rerun(): voidrerun}>Rerun CI</button>import type { type RequestHandler = (event: RequestEvent<Record<string, any>, string | null>) => MaybePromise<Response>RequestHandler } from './$types';
export const POST: type RequestHandler = (event: RequestEvent<Record<string, any>, string | null>) => MaybePromise<Response>RequestHandler = () => { // do something
};/** @type {import('./$types').RequestHandler} */
export function POST() { // do something
}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" (or, equivalently, no method at all), and SvelteKit will treat them like <a> elements, using the client-side router instead of a full page navigation:
<form action="/search">
<label>
Search
<input name="q">
</label>
</form>Submitting this form will navigate to /search?q=... and invoke your load function but will not invoke an action. 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 behaviour.