Skip to content
← All frameworks
svelte · SvelteKit

Svelte framework

SvelteKit form handling without a backend

SvelteKit has form actions and server routes, but they put you back in the business of running and securing a backend for every contact form. If all you need is to receive submissions, email them, and keep the data in the EU, post directly to Formward instead and leave your SvelteKit routes purely presentational.

The component below is a client-side enhancement: it intercepts the submit, sends FormData with fetch(), and updates a reactive status. It pairs naturally with progressive enhancement - the same markup would POST normally if scripting were disabled, since the form action points at Formward.

EU-hosted · Keep SvelteKit for the UI and let an EU form backend own the data: submissions stay in Sweden, with a DPA available for your records.

The SvelteKit example

Copy this into your project and replace <FORM_ID> with the id of a form you create in the Formward dashboard.

svelte

<script>
  let status = "idle"; // idle | sending | ok | error

  async function onSubmit(e) {
    status = "sending";
    const form = e.target;
    try {
      const res = await fetch(form.action, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(form),
      });
      const data = await res.json(); // { ok: true, id, files }
      if (!res.ok || !data.ok) throw new Error(data.error || res.status);
      form.reset();
      status = "ok";
    } catch {
      status = "error";
    }
  }
</script>

{#if status === "ok"}
  <p>Thanks - your message is on its way.</p>
{:else}
  <form action="https://forms.formward.eu/f/<FORM_ID>" method="POST" on:submit|preventDefault={onSubmit}>
    <input type="email" name="email" required />
    <textarea name="message" required></textarea>
    <input type="text" name="_gotcha" tabindex="-1" autocomplete="off"
      style="display:none" aria-hidden="true" />
    <button disabled={status === "sending"}>
      {status === "sending" ? "Sending…" : "Send"}
    </button>
    {#if status === "error"}<p role="alert">Could not send. Try again.</p>{/if}
    <p>We only use your details to reply. <a href="/privacy">Privacy policy</a></p>
  </form>
{/if}

Success and error handling

  • Because the <form> carries action and method, it still submits if JavaScript fails to load - Formward returns a 302 redirect in that no-JS path.
  • Reading form.action in the handler keeps the URL in one place; change the endpoint once in the markup and the fetch() follows.
  • No +page.server.js action and no +server.js endpoint are involved, so there is nothing server-side to deploy, rate-limit, or secure.

Spam protection

Every example above includes the _gotcha honeypot field. It is hidden from real users and must stay empty. When a bot fills it, Formward files the submission as spam and still answers with a normal success response, so the bot learns nothing and nothing reaches your inbox. Rate limits and the origin allowlist are checked on Formward's side as well. For a stricter gate, switch on Cloudflare Turnstile in the form's settings and add the widget; it adds a cf-turnstile-response field that Formward verifies on receipt. Turnstile is optional, and Cloudflare is a US provider, listed as such in the sub-processor register. The spam docs have the widget markup.

The JSON response

A standard POST gets a 302 redirect (or your thank-you page). Send the Accept: application/json header - as every fetch() example above does - and Formward returns JSON instead:

HTTP/1.1 200 OK
Content-Type: application/json

{ "ok": true, "id": "3f2a9c1e-...", "files": 0, "success": { "title": "Thank you!", ... } }

files is the number of uploaded files stored and success carries the confirmation text from the form's settings. When you set a redirect URL for the form, the reply also has a redirect field your code can send the visitor to. On failure the body is { ok: false, error } with a 4xx status: 422 when required consent is missing or a field is too long, 403 when a Turnstile check fails, 429 for too many submissions from one address, 402 when the account is over its allowance and its Over limit folder is full. See responses and redirects for the full table and the AJAX docs for CORS.

Other frameworks

Collect your first SvelteKit submission

Create a form, paste the snippet, and keep every submission in the EU. GDPR-clean from the first POST.