# SvelteKit remote functions - a validated form without form actions

Remote functions let a SvelteKit form post to a typed server function instead of a form action. vela generate form --remote scaffolds the form, the remote function, the Zod schema and the config flags so you can try the new model in a minute.

Pattern: [Generate a form (remote functions)](https://velastack.dev/patterns/generate-form-remote) · Docs: https://docs.velastack.dev/generate/form

[Remote functions](https://svelte.dev/docs/kit/remote-functions) are SvelteKit's newer way to call the server from a component: you export a `form()`, `query()` or `command()` from a `.remote.ts` file and use it directly in markup, with the schema doing validation on both sides. For forms it replaces the `actions` export, `superValidate` and the form store with a single function and a spread. It is still experimental, which is exactly when a generator is useful: `vela generate form --remote` gives you a working example with the config flags already flipped.

## Prerequisites

A SvelteKit project with the `vela` CLI:

```sh
$ npx vela create my-app
```

or, for an existing project, `npx vela bless`. See [Coming from vanilla SvelteKit](https://docs.velastack.dev/bless). The route below lands under `(app)` because [auth](https://velastack.dev/tutorials/sveltekit-login-and-signup) is enabled in this project.

> **Note:**
> The command also runs in a plain `npx sv create` project with no setup. It installs Zod and flips the same config flags; the route lands at `src/routes/contact`, the page is plain HTML with native inputs instead of shadcn-svelte components, and success shows as a status line on the page rather than a toast. The server test is only written when the project has the test harness, which `vela enable backend` adds.

## Run the command

Same field list as the [classic form generator](https://velastack.dev/tutorials/sveltekit-form-validation-zod-superforms), plus `--remote`:

```sh
$ vela generate form --remote contact name:text! email:email! message:editor
✔ Created 4 files, modified 1
  src/lib/schemas/contact.ts
  src/routes/(app)/contact/+page.svelte
  src/routes/(app)/contact/form.remote.ts
  src/routes/(app)/contact/server.test.ts
  svelte.config.js
```

## What was generated

### The config

Remote functions and the async compiler option are both behind flags. The generator adds them to `svelte.config.js` if they are missing:

**svelte.config.js**

```js
import adapter from "@sveltejs/adapter-node";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter,
    // [!code highlight:3]
    experimental: {
      remoteFunctions: true,
    },
  },
  // [!code highlight:5]
  compilerOptions: {
    experimental: {
      async: true,
    },
  },
};

export default config;
```

### The remote function

Where the classic version has a `+page.server.ts` with a `load` and an `actions` export, the remote version has one function. The schema is passed as the first argument, so `data` arrives already validated and typed:

**src/routes/(app)/contact/form.remote.ts**

```ts
import { form, getRequestEvent } from "$app/server";
import { setFlash } from "sveltekit-flash-message/server";
import { contactSchema } from "$lib/schemas/contact";

export const submitContactForm = form(contactSchema, async (data) => {
  const { cookies } = getRequestEvent();
  setFlash({ type: "toast", message: "Form posted successfully" }, cookies);
  return { success: true };
});
```

`getRequestEvent()` is how a remote function reaches `cookies`, `locals` and the rest of the request.

### The page

No `superForm`, no store. The form is spread onto the `<form>` element, each input is spread from `fields.<name>.as(type)`, and validation issues are read from `fields.<name>.issues()`:

**src/routes/(app)/contact/+page.svelte**

```svelte
<script lang="ts">
  import { submitContactForm } from "./form.remote";
  import { Button } from "$lib/components/ui/button";
  import { Input } from "$lib/components/ui/input";
  import { Textarea } from "$lib/components/ui/textarea";
</script>

<section data-role="content">
  <div class="flex justify-between items-center mb-4">
    <h1 class="text-3xl font-bold tracking-tight">Contact</h1>
  </div>
  <div class="bg-card rounded-lg shadow-sm border p-4">
    <form {...submitContactForm}>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
        <div class="space-y-2 col-span-1">
          <label for="name" class="text-sm font-medium">Name</label>
          <Input
            id="name"
            {...submitContactForm.fields.name.as("text")}
            required
          />
          {#each submitContactForm.fields.name.issues() as issue}
            <p class="text-destructive text-sm">{issue.message}</p>
          {/each}
        </div>
        <div class="space-y-2 col-span-1">
          <label for="email" class="text-sm font-medium">Email</label>
          <Input
            id="email"
            {...submitContactForm.fields.email.as("text")}
            type="email"
            required
          />
          {#each submitContactForm.fields.email.issues() as issue}
            <p class="text-destructive text-sm">{issue.message}</p>
          {/each}
        </div>
        <div class="space-y-2 col-span-2">
          <label for="message" class="text-sm font-medium">Message</label>
          <Textarea
            id="message"
            {...submitContactForm.fields.message.as("text")}
          />
          {#each submitContactForm.fields.message.issues() as issue}
            <p class="text-destructive text-sm">{issue.message}</p>
          {/each}
        </div>
      </div>

      <div class="mt-4 flex gap-2 border-t pt-4 -mx-4 px-4">
        <Button type="submit">Submit</Button>
      </div>
    </form>
  </div>
</section>
```

### The schema and the test

The Zod schema is identical to the classic generator's, and the server test posts to the remote function's endpoint the same way it would post to an action.

**src/lib/schemas/contact.ts**

```ts
import { z } from "zod";

export const contactSchema = z.object({
  name: z.string().nonempty(),
  email: z.email(),
  message: z.string().optional(),
});
```

## Try it

```sh
$ vela dev
```

Open [localhost:5173/contact](http://localhost:5173/contact). Submitting an invalid form renders the issues next to the fields; a valid one shows the toast.

## Make it yours

Everything the form should do goes inside the remote function. Storing the message in a collection created with [the resource generator](https://velastack.dev/tutorials/sveltekit-database-model-from-cli) is two lines:

**src/routes/(app)/contact/form.remote.ts**

```ts
import { form, getRequestEvent } from "$app/server";
import { setFlash } from "sveltekit-flash-message/server";
import { contactSchema } from "$lib/schemas/contact";

export const submitContactForm = form(contactSchema, async (data) => {
  const { cookies, locals } = getRequestEvent();
  await locals.admin.collection("messages").create(data);
  setFlash({ type: "toast", message: "Thanks, we'll be in touch" }, cookies);
  return { success: true };
});
```

> **Note:**
> Remote functions are experimental in SvelteKit and their API can still change between minor versions. The generated code targets the version of SvelteKit in your project; if you upgrade and something breaks, regenerate the form or compare against the [pattern page](https://velastack.dev/patterns/generate-form-remote), which always shows the current output.

## Tests

```sh
$ npm run test:server
```

The generated `server.test.ts` covers the page and a valid submission, so a schema change that breaks the form fails in CI.

## Going further

- [Form actions vs remote functions](https://velastack.dev/tutorials/sveltekit-form-actions-vs-remote-functions) compares both generators side by side
- [CRUD with remote functions](https://velastack.dev/tutorials/sveltekit-crud-remote-functions) applies the same idea to a whole resource
- Undo with `vela destroy form contact`
- [Pattern page: Form with remote functions](https://velastack.dev/patterns/generate-form-remote) · [Docs](https://docs.velastack.dev/generate/form)
