Form actions
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:
/** @satisfies {import('./$types').Actions} */
export const const actions: {
default: (event: any) => Promise<void>;
}
actions = {
default: (event: any) => Promise<void>
default: async (event: any
event) => {
// TODO log the user in
}
};
import type { type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions } from './$types';
export const const actions: {
default: (event: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
default: (event: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>
default: async (event: Kit.RequestEvent<Record<string, any>, string | null>
event) => {
// TODO log the user in
}
} satisfies type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions;
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
POST
requests, sinceGET
requests 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:
/** @satisfies {import('./$types').Actions} */
export const const actions: {
login: (event: any) => Promise<void>;
register: (event: any) => Promise<void>;
}
actions = {
default: async (event) => {
login: (event: any) => Promise<void>
login: async (event: any
event) => {
// TODO log the user in
},
register: (event: any) => Promise<void>
register: async (event: any
event) => {
// TODO register the user
}
};
import type { type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions } from './$types';
export const const actions: {
login: (event: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>;
register: (event: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<...>;
}
actions = {
default: async (event) => {
login: (event: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>
login: async (event: Kit.RequestEvent<Record<string, any>, string | null>
event) => {
// TODO log the user in
},
register: (event: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>
register: async (event: Kit.RequestEvent<Record<string, any>, string | null>
event) => {
// TODO register the user
}
} satisfies type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions;
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.
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: Cookies
Get or set cookies related to the current request
cookies }) {
const const user: any
user = await module "$lib/server/db"
db.getUserFromSession(cookies: Cookies
Get or set cookies related to the current request
cookies.Cookies.get(name: string, opts?: CookieParseOptions): string | undefined
Gets a cookie that was previously set with cookies.set
, or from the request headers.
get('sessionid'));
return { user: any
user };
}
/** @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<...>;
}
actions = {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<{
success: boolean;
}>
login: async ({ cookies: Cookies
Get or set cookies related to the current request
cookies, request: Request
The original request object
request }) => {
const const data: FormData
data = await request: Request
The original request object
request.Body.formData(): Promise<FormData>
formData();
const const email: FormDataEntryValue | null
email = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('email');
const const password: FormDataEntryValue | null
password = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('password');
const const user: any
user = await module "$lib/server/db"
db.getUser(const email: FormDataEntryValue | null
email);
cookies: Cookies
Get 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: any
user), { path: string
Specifies 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: boolean
success: 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
}
};
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: PageServerLoad
load: type PageServerLoad = (event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad = async ({ cookies: Cookies
Get or set cookies related to the current request
cookies }) => {
const const user: any
user = await module "$lib/server/db"
db.getUserFromSession(cookies: Cookies
Get or set cookies related to the current request
cookies.Cookies.get(name: string, opts?: CookieParseOptions): string | undefined
Gets a cookie that was previously set with cookies.set
, or from the request headers.
get('sessionid'));
return { user: any
user };
};
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<...>;
}
actions = {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<{
success: boolean;
}>
login: async ({ cookies: Cookies
Get or set cookies related to the current request
cookies, request: Request
The original request object
request }) => {
const const data: FormData
data = await request: Request
The original request object
request.Body.formData(): Promise<FormData>
formData();
const const email: FormDataEntryValue | null
email = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('email');
const const password: FormDataEntryValue | null
password = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('password');
const const user: any
user = await module "$lib/server/db"
db.getUser(const email: FormDataEntryValue | null
email);
cookies: Cookies
Get 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: any
user), { path: string
Specifies 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: boolean
success: 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;
<script>
/** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */
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}
<script lang="ts">
import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData, form: ActionData } = $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
In Svelte 4, you’d use
export let data
andexport let form
instead 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
:
import { function fail(status: number): ActionFailure<undefined> (+1 overload)
Create an ActionFailure
object.
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<{
...;
}> | {
...;
}>;
register: (event: RequestEvent<...>) => Promise<...>;
}
actions = {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: string | null;
missing: boolean;
}> | ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> | {
...;
}>
login: async ({ cookies: Cookies
Get or set cookies related to the current request
cookies, request: Request
The original request object
request }) => {
const const data: FormData
data = await request: Request
The original request object
request.Body.formData(): Promise<FormData>
formData();
const const email: FormDataEntryValue | null
email = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('email');
const const password: FormDataEntryValue | null
password = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('password');
if (!const email: FormDataEntryValue | null
email) {
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.
email: string | null
email, missing: boolean
missing: true });
}
const const user: any
user = await module "$lib/server/db"
db.getUser(const email: FormDataEntryValue
email);
if (!const user: any
user || const user: any
user.password !== module "$lib/server/db"
db.hash(const password: FormDataEntryValue | null
password)) {
return fail<{
email: FormDataEntryValue;
incorrect: boolean;
}>(status: number, data: {
email: FormDataEntryValue;
incorrect: boolean;
}): ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> (+1 overload)Create an ActionFailure
object.
email: FormDataEntryValue
email, incorrect: boolean
incorrect: true });
}
cookies: Cookies
Get 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: any
user), { path: string
Specifies 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: boolean
success: 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
}
};import { function fail(status: number): ActionFailure<undefined> (+1 overload)
Create an ActionFailure
object.
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<{
...;
}> | {
...;
}>;
register: (event: RequestEvent<...>) => Promise<...>;
}
actions = {
login: ({ cookies, request }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: string | null;
missing: boolean;
}> | ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> | {
...;
}>
login: async ({ cookies: Cookies
Get or set cookies related to the current request
cookies, request: Request
The original request object
request }) => {
const const data: FormData
data = await request: Request
The original request object
request.Body.formData(): Promise<FormData>
formData();
const const email: FormDataEntryValue | null
email = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('email');
const const password: FormDataEntryValue | null
password = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('password');
if (!const email: FormDataEntryValue | null
email) {
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.
email: string | null
email, missing: boolean
missing: true });
}
const const user: any
user = await module "$lib/server/db"
db.getUser(const email: FormDataEntryValue
email);
if (!const user: any
user || const user: any
user.password !== module "$lib/server/db"
db.hash(const password: FormDataEntryValue | null
password)) {
return fail<{
email: FormDataEntryValue;
incorrect: boolean;
}>(status: number, data: {
email: FormDataEntryValue;
incorrect: boolean;
}): ActionFailure<{
email: FormDataEntryValue;
incorrect: boolean;
}> (+1 overload)Create an ActionFailure
object.
email: FormDataEntryValue
email, incorrect: boolean
incorrect: true });
}
cookies: Cookies
Get 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: any
user), { path: string
Specifies 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: boolean
success: 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;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
:
import { function fail(status: number): ActionFailure<undefined> (+1 overload)
Create an ActionFailure
object.
fail, function redirect(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), location: string | URL): never
Redirect a request. When called during request handling, SvelteKit will return a redirect response.
Make sure you’re not catching the thrown redirect, which would prevent SvelteKit from handling it.
redirect } 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, url }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: FormDataEntryValue | null;
missing: boolean;
}> | ActionFailure<...> | {
...;
}>;
register: (event: RequestEvent<...>) => Promise<...>;
}
actions = {
login: ({ cookies, request, url }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: FormDataEntryValue | null;
missing: boolean;
}> | ActionFailure<...> | {
...;
}>
login: async ({ cookies: Cookies
Get or set cookies related to the current request
cookies, request: Request
The original request object
request, url: URL
The requested URL.
url }) => {
const const data: FormData
data = await request: Request
The original request object
request.Body.formData(): Promise<FormData>
formData();
const const email: FormDataEntryValue | null
email = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('email');
const const password: FormDataEntryValue | null
password = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('password');
const const user: any
user = await module "$lib/server/db"
db.getUser(const email: FormDataEntryValue | null
email);
if (!const user: any
user) {
return fail<{
email: FormDataEntryValue | null;
missing: boolean;
}>(status: number, data: {
email: FormDataEntryValue | null;
missing: boolean;
}): ActionFailure<...> (+1 overload)
Create an ActionFailure
object.
fail(400, { email: FormDataEntryValue | null
email, missing: boolean
missing: true });
}
if (const user: any
user.password !== module "$lib/server/db"
db.hash(const password: FormDataEntryValue | null
password)) {
return fail<{
email: FormDataEntryValue | null;
incorrect: boolean;
}>(status: number, data: {
email: FormDataEntryValue | null;
incorrect: boolean;
}): ActionFailure<...> (+1 overload)
Create an ActionFailure
object.
fail(400, { email: FormDataEntryValue | null
email, incorrect: boolean
incorrect: true });
}
cookies: Cookies
Get 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: any
user), { path: string
Specifies 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: '/' });
if (url: URL
The requested URL.
url.URL.searchParams: URLSearchParams
searchParams.URLSearchParams.has(name: string, value?: string): boolean
Returns a Boolean indicating if such a search parameter exists.
has('redirectTo')) {
function redirect(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), location: string | URL): never
Redirect a request. When called during request handling, SvelteKit will return a redirect response.
Make sure you’re not catching the thrown redirect, which would prevent SvelteKit from handling it.
redirect(303, url: URL
The requested URL.
url.URL.searchParams: URLSearchParams
searchParams.URLSearchParams.get(name: string): string | null
Returns the first value associated to the given search parameter.
get('redirectTo'));
}
return { success: boolean
success: 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
}
};
import { function fail(status: number): ActionFailure<undefined> (+1 overload)
Create an ActionFailure
object.
fail, function redirect(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), location: string | URL): never
Redirect a request. When called during request handling, SvelteKit will return a redirect response.
Make sure you’re not catching the thrown redirect, which would prevent SvelteKit from handling it.
redirect } 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, url }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: FormDataEntryValue | null;
missing: boolean;
}> | ActionFailure<...> | {
...;
}>;
register: (event: RequestEvent<...>) => Promise<...>;
}
actions = {
login: ({ cookies, request, url }: RequestEvent<Record<string, any>, string | null>) => Promise<ActionFailure<{
email: FormDataEntryValue | null;
missing: boolean;
}> | ActionFailure<...> | {
...;
}>
login: async ({ cookies: Cookies
Get or set cookies related to the current request
cookies, request: Request
The original request object
request, url: URL
The requested URL.
url }) => {
const const data: FormData
data = await request: Request
The original request object
request.Body.formData(): Promise<FormData>
formData();
const const email: FormDataEntryValue | null
email = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('email');
const const password: FormDataEntryValue | null
password = const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('password');
const const user: any
user = await module "$lib/server/db"
db.getUser(const email: FormDataEntryValue | null
email);
if (!const user: any
user) {
return fail<{
email: FormDataEntryValue | null;
missing: boolean;
}>(status: number, data: {
email: FormDataEntryValue | null;
missing: boolean;
}): ActionFailure<...> (+1 overload)
Create an ActionFailure
object.
fail(400, { email: FormDataEntryValue | null
email, missing: boolean
missing: true });
}
if (const user: any
user.password !== module "$lib/server/db"
db.hash(const password: FormDataEntryValue | null
password)) {
return fail<{
email: FormDataEntryValue | null;
incorrect: boolean;
}>(status: number, data: {
email: FormDataEntryValue | null;
incorrect: boolean;
}): ActionFailure<...> (+1 overload)
Create an ActionFailure
object.
fail(400, { email: FormDataEntryValue | null
email, incorrect: boolean
incorrect: true });
}
cookies: Cookies
Get 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: any
user), { path: string
Specifies 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: '/' });
if (url: URL
The requested URL.
url.URL.searchParams: URLSearchParams
searchParams.URLSearchParams.has(name: string, value?: string): boolean
Returns a Boolean indicating if such a search parameter exists.
has('redirectTo')) {
function redirect(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), location: string | URL): never
Redirect a request. When called during request handling, SvelteKit will return a redirect response.
Make sure you’re not catching the thrown redirect, which would prevent SvelteKit from handling it.
redirect(303, url.searchParams.get('redirectTo'));
}
return { success: boolean
success: 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;
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:
/** @type {import('@sveltejs/kit').Handle} */
export async function function handle(input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}): MaybePromise<...>
handle({ event: RequestEvent<Partial<Record<string, string>>, string | null>
event, resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>
resolve }) {
event: RequestEvent<Partial<Record<string, string>>, string | null>
event.RequestEvent<Partial<Record<string, string>>, string | null>.locals: App.Locals
Contains 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<Partial<Record<string, string>>, string | null>
event.RequestEvent<Partial<Record<string, string>>, string | null>.cookies: Cookies
Get or set cookies related to the current request
cookies.Cookies.get(name: string, opts?: CookieParseOptions): string | undefined
Gets 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<Partial<Record<string, string>>, string | null>
event);
}
import type { type Handle = (input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}) => MaybePromise<...>
The handle
hook runs every time the SvelteKit server receives a request and
determines the response.
It receives an event
object representing the request and a function called resolve
, which renders the route and generates a Response
.
This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).
Handle } from '@sveltejs/kit';
export const const handle: Handle
handle: type Handle = (input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}) => MaybePromise<...>
The handle
hook runs every time the SvelteKit server receives a request and
determines the response.
It receives an event
object representing the request and a function called resolve
, which renders the route and generates a Response
.
This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).
Handle = async ({ event: RequestEvent<Partial<Record<string, string>>, string | null>
event, resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>
resolve }) => {
event: RequestEvent<Partial<Record<string, string>>, string | null>
event.RequestEvent<Partial<Record<string, string>>, string | null>.locals: App.Locals
Contains 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<Partial<Record<string, string>>, string | null>
event.RequestEvent<Partial<Record<string, string>>, string | null>.cookies: Cookies
Get or set cookies related to the current request
cookies.Cookies.get(name: string, opts?: CookieParseOptions): string | undefined
Gets 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<Partial<Record<string, string>>, string | null>
event);
};
/** @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.Locals
Contains 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: Cookies
Get 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: string
Specifies 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 Partial<Record<string, string>> = Partial<Record<string, string>>, RouteId extends string | null = string | null>.locals: App.Locals
Contains custom data that was added to the request within the server handle hook
.
locals.App.Locals.user: {
name: string;
} | null
user = null;
}
};
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: PageServerLoad
load: type PageServerLoad = (event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad = (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.Locals
Contains custom data that was added to the request within the server handle hook
.
locals.App.Locals.user: {
name: string;
} | null
user
};
};
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: Cookies
Get 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: string
Specifies 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 Partial<Record<string, string>> = Partial<Record<string, string>>, RouteId extends string | null = string | null>.locals: App.Locals
Contains custom data that was added to the request within the server handle hook
.
locals.App.Locals.user: {
name: string;
} | null
user = null;
}
} satisfies type Actions = {
[x: string]: Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions;
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>
import { enhance } from '$app/forms';
/** @type {{ form: import('./$types').ActionData }} */
let { form } = $props();
</script>
<form method="POST" use:enhance>
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
</script>
<form method="POST" use:enhance>
use:enhance
can only be used with forms that havemethod="POST"
. It will not work withmethod="GET"
, which is the default for forms without a specified method. Attempting to useuse:enhance
on forms withoutmethod="POST"
will result in an error.
Yes, it’s a little confusing that the
enhance
action 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
form
property,page.form
andpage.status
on 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" ..>
, theform
prop and thepage.form
state 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
invalidateAll
on a successful response - call
goto
on a redirect response - render the nearest
+error
boundary 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
. Note that if you return a callback, the default behavior mentioned above is not triggered. To get it back, call update
.
<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 may need to reproduce part of the default use:enhance
behaviour, but without invalidating all data on a successful response. You can do so with applyAction
:
<script>
import { enhance, applyAction } from '$app/forms';
/** @type {{ form: import('./$types').ActionData }} */
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);
}
};
}}
>
<script lang="ts">
import { enhance, applyAction } from '$app/forms';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $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.status
toresult.status
and updatesform
andpage.form
toresult.data
(regardless of where you are submitting from, in contrast toupdate
fromenhance
)redirect
— callsgoto(result.location, { invalidateAll: true })
error
— renders the nearest+error
boundary 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>
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';
/** @type {{ form: import('./$types').ActionData }} */
let { form } = $props();
/** @param {SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}} event */
async function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
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>
<script lang="ts">
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';
import type { ActionData } from './$types';
import type { ActionResult } from '@sveltejs/kit';
let { form }: { form: ActionData } = $props();
async function handleSubmit(event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}) {
event.preventDefault();
const data = new FormData(event.currentTarget);
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>
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: Response
response = await function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response> (+1 overload)
fetch(this.action, {
RequestInit.method?: string | undefined
A string to set request’s method.
method: 'POST',
RequestInit.body?: BodyInit | null | undefined
A BodyInit object or null to set request’s body.
body: data,
RequestInit.headers?: HeadersInit | undefined
A 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>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>
<button onclick={rerun}>Rerun CI</button>
<script lang="ts">
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>
<button onclick={rerun}>Rerun CI</button>
/** @type {import('./$types').RequestHandler} */
export function function POST(): void
POST() {
// do something
}
import type { type RequestHandler = (event: Kit.RequestEvent<Record<string, any>, string | null>) => MaybePromise<Response>
type RequestHandler = (event: Kit.RequestEvent<Record<string, any>, string | null>) => MaybePromise<Response>
RequestHandler } from './$types';
export const const POST: RequestHandler
POST: type RequestHandler = (event: Kit.RequestEvent<Record<string, any>, string | null>) => MaybePromise<Response>
type RequestHandler = (event: Kit.RequestEvent<Record<string, any>, string | null>) => MaybePromise<Response>
RequestHandler = () => {
// 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.