← All frameworks

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>Thanksyour 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}
  </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; Formward silently drops any submission where it is filled, which stops most bots with no CAPTCHA. For a stricter gate, add a Cloudflare Turnstile widget and send its token as the cf-turnstile-response field — Formward verifies it on receipt.

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": "clxyz123...", "files": [] }

On failure the body is { ok: false, error } with a 4xx status (validation, plan limit, and so on). See the AJAX docs for the full status-code table and CORS notes.

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.

SvelteKit form handling without a backend | Formward