Remote functions are a tool for type-safe communication between client and server. They can be _called_ anywhere in your app, but always _run_ on the server, meaning they can safely access [server-only modules](server-only-modules) containing things like environment variables and database clients.
Combined with Svelte's experimental support for [`await`](/docs/svelte/await-expressions), it allows you to load and manipulate data directly inside your components.
This feature is currently experimental, meaning it is likely to contain bugs and is subject to change without notice. You must opt in by adding the `kit.experimental.remoteFunctions` option in your `svelte.config.js` and optionally, the `compilerOptions.experimental.async` option to use `await` in components:
```js
/// file: svelte.config.js
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
experimental: {
+++remoteFunctions: true+++
}
},
compilerOptions: {
experimental: {
+++async: true+++
}
}
};
export default config;
```
## Overview
Remote functions are exported from a `.remote.js` or `.remote.ts` file, and come in four flavours: `query`, `form`, `command` and `prerender`. On the client, the exported functions are transformed to `fetch` wrappers that invoke their counterparts on the server via a generated HTTP endpoint. Remote files must be placed in your `src` directory.
## query
The `query` function allows you to read dynamic data from the server (for _static_ data, consider using [`prerender`](#prerender) instead):
```js
/// file: src/routes/blog/data.remote.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function sql(strings: TemplateStringsArray, ...values: any[]): Promise;
}
// @filename: index.js
// ---cut---
import { query } from '$app/server';
import * as db from '$lib/server/database';
export const getPosts = query(async () => {
const posts = await db.sql`
SELECT title, slug
FROM post
ORDER BY published_at
DESC
`;
return posts;
});
```
> [!NOTE] Throughout this page, you'll see imports from fictional modules like `$lib/server/database` and `$lib/server/auth`. These are purely for illustrative purposes — you can use whatever database client and auth setup you like.
>
> The `db.sql` function above is a [tagged template function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) that escapes any interpolated values.
The query returned from `getPosts` works as a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves to `posts`:
```svelte
```
Until the promise resolves — and if it errors — the nearest [``](../svelte/svelte-boundary) will be invoked.
While using `await` is recommended, as an alternative the query also has `loading`, `error` and `current` properties:
```svelte
{/if}
```
> [!NOTE] For the rest of this document, we'll use the `await` form.
### Query arguments
Query functions can accept an argument, such as the `slug` of an individual post:
```svelte
{post.title}
{@html post.content}
```
Since `getPost` exposes an HTTP endpoint, it's important to validate this argument to be sure that it's the correct type. For this, we can use any [Standard Schema](https://standardschema.dev/) validation library such as [Zod](https://zod.dev/) or [Valibot](https://valibot.dev/):
```js
/// file: src/routes/blog/data.remote.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function sql(strings: TemplateStringsArray, ...values: any[]): Promise;
}
// @filename: index.js
// ---cut---
import * as v from 'valibot';
import { error } from '@sveltejs/kit';
import { query } from '$app/server';
import * as db from '$lib/server/database';
export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => {
const [post] = await db.sql`
SELECT * FROM post
WHERE slug = ${slug}
`;
if (!post) error(404, 'Not found');
return post;
});
```
Both the argument and the return value are serialized with [devalue](https://github.com/sveltejs/devalue), which handles types like `Date` and `Map` (and custom types defined in your [transport hook](hooks#Universal-hooks-transport)) in addition to JSON.
### Refreshing queries
Any query can be re-fetched via its `refresh` method, which retrieves the latest value from the server:
```svelte
```
> [!NOTE] Queries are cached while they're on the page, meaning `getPosts() === getPosts()`. This means you don't need a reference like `const posts = getPosts()` in order to update the query.
## query.batch
`query.batch` works like `query` except that it batches requests that happen within the same macrotask. This solves the so-called n+1 problem: rather than each query resulting in a separate database call (for example), simultaneous queries are grouped together.
On the server, the callback receives an array of the arguments the function was called with. It must return a function of the form `(input: Input, index: number) => Output`. SvelteKit will then call this with each of the input arguments to resolve the individual calls with their results.
```js
/// file: weather.remote.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function sql(strings: TemplateStringsArray, ...values: any[]): Promise;
}
// @filename: index.js
// ---cut---
import * as v from 'valibot';
import { query } from '$app/server';
import * as db from '$lib/server/database';
export const getWeather = query.batch(v.string(), async (cities) => {
const weather = await db.sql`
SELECT * FROM weather
WHERE city = ANY(${cities})
`;
const lookup = new Map(weather.map(w => [w.city, w]));
return (city) => lookup.get(city);
});
```
```svelte
Weather
{#each cities.slice(0, limit) as city}
{city.name}
{/each}
{#if cities.length > limit}
{/if}
```
## form
The `form` function makes it easy to write data to the server. It takes a callback that receives `data` constructed from the submitted [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)...
```ts
/// file: src/routes/blog/data.remote.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function sql(strings: TemplateStringsArray, ...values: any[]): Promise;
}
declare module '$lib/server/auth' {
interface User {
name: string;
}
/**
* Gets a user's info from their cookies, using `getRequestEvent`
*/
export function getUser(): Promise;
}
// @filename: index.js
// ---cut---
import * as v from 'valibot';
import { error, redirect } from '@sveltejs/kit';
import { query, form } from '$app/server';
import * as db from '$lib/server/database';
import * as auth from '$lib/server/auth';
export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => { /* ... */ });
export const createPost = form(
v.object({
title: v.pipe(v.string(), v.nonEmpty()),
content:v.pipe(v.string(), v.nonEmpty())
}),
async ({ title, content }) => {
// Check the user is logged in
const user = await auth.getUser();
if (!user) error(401, 'Unauthorized');
const slug = title.toLowerCase().replace(/ /g, '-');
// Insert into the database
await db.sql`
INSERT INTO post (slug, title, content)
VALUES (${slug}, ${title}, ${content})
`;
// Redirect to the newly created page
redirect(303, `/blog/${slug}`);
}
);
```
...and returns an object that can be spread onto a `
```
As with `query`, if the callback uses the submitted `data`, it should be [validated](#query-Query-arguments) by passing a [Standard Schema](https://standardschema.dev) as the first argument to `form`. The one difference is to `query` is that the schema inputs must all be of type `string` or `File`, since that's all the original `FormData` provides. You can however coerce the value into a different type — how to do that depends on the validation library you use.
```ts
/// file: src/routes/count.remote.js
import * as v from 'valibot';
import { form } from '$app/server';
export const setCount = form(
v.object({
// Valibot:
count: v.pipe(v.string(), v.transform((s) => Number(s)), v.number()),
// Zod:
// count: z.coerce.number()
}),
async ({ count }) => {
// ...
}
);
```
The `name` attributes on the form controls must correspond to the properties of the schema — `title` and `content` in this case. If you schema contains objects, use object notation:
```svelte
{#each jobs as job, idx}
{/each}
```
To indicate a repeated field, use a `[]` suffix:
```svelte
```
If you'd like type safety and autocomplete when setting `name` attributes, use the form object's `field` method:
```svelte
```
This will error during typechecking if `title` does not exist on your schema.
The form object contains `method` and `action` properties that allow it to work without JavaScript (i.e. it submits data and reloads the page). It also has an [attachment](/docs/svelte/@attach) that progressively enhances the form when JavaScript is available, submitting data *without* reloading the entire page.
### Validation
If the submitted data doesn't pass the schema, the callback will not run. Instead, the form object's `issues` object will be populated:
```svelte
```
You don't need to wait until the form is submitted to validate the data — you can call `validate()` programmatically, for example in an `oninput` callback (which will validate the data on every keystroke) or an `onchange` callback:
```svelte
```
By default, issues will be ignored if they belong to form controls that haven't yet been interacted with. To validate _all_ inputs, call `validate({ includeUntouched: true })`.
For client-side validation, you can specify a _preflight_ schema which will populate `issues` and prevent data being sent to the server if the data doesn't validate:
```svelte
Create a new post
```
> [!NOTE] The preflight schema can be the same object as your server-side schema, if appropriate, though it won't be able to do server-side checks like 'this value already exists in the database'. Note that you cannot export a schema from a `.remote.ts` or `.remote.js` file, so the schema must either be exported from a shared module, or from a `
Create a new post
{#if createPost.result?.success}
Successfully published!
{/if}
```
This value is _ephemeral_ — it will vanish if you resubmit, navigate away, or reload the page.
> [!NOTE] The `result` value need not indicate success — it can also contain validation errors, along with any data that should repopulate the form on page reload.
If an error occurs during submission, the nearest `+error.svelte` page will be rendered.
### enhance
We can customize what happens when the form is submitted with the `enhance` method:
```svelte
Create a new post
```
The callback receives the `form` element, the `data` it contains, and a `submit` function.
To enable client-driven [single-flight mutations](#form-Single-flight-mutations), use `submit().updates(...)`. For example, if the `getPosts()` query was used on this page, we could refresh it like so:
```ts
import type { RemoteQuery, RemoteQueryOverride } from '@sveltejs/kit';
interface Post {}
declare function submit(): Promise & {
updates(...queries: Array | RemoteQueryOverride>): Promise;
}
declare function getPosts(): RemoteQuery;
// ---cut---
await submit().updates(getPosts());
```
We can also _override_ the current data while the submission is ongoing:
```ts
import type { RemoteQuery, RemoteQueryOverride } from '@sveltejs/kit';
interface Post {}
declare function submit(): Promise & {
updates(...queries: Array | RemoteQueryOverride>): Promise;
}
declare function getPosts(): RemoteQuery;
declare const newPost: Post;
// ---cut---
await submit().updates(
getPosts().withOverride((posts) => [newPost, ...posts])
);
```
The override will be applied immediately, and released when the submission completes (or fails).
### buttonProps
By default, submitting a form will send a request to the URL indicated by the `