Skip to content
← All frameworks
vue · Nuxt

Vue framework

Nuxt form handling without a backend

Nuxt gives you a Nitro server, but you rarely want to own server-side form handling - parsing bodies, sending mail, persisting data, and minding GDPR. Skip all of it by posting from the client straight to Formward, an EU-hosted form backend. Your Nuxt app can even be statically generated with nuxi generate and the form still works.

The component below is ordinary Vue inside Nuxt. It uses the native fetch() (no need for $fetch here, since the request leaves your app entirely) and a reactive status ref to drive the sending, success, and error UI.

EU-hosted · Whether you deploy Nuxt as a server or a static export, submissions are received and stored in Sweden - Formward processes your form data in the EU/EEA.

The Nuxt example

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

vue

<script setup>
const status = ref("idle"); // idle | sending | ok | error

async function onSubmit(e) {
  status.value = "sending";
  const form = e.target;
  try {
    const res = await fetch("https://forms.formward.eu/f/<FORM_ID>", {
      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.value = "ok";
  } catch {
    status.value = "error";
  }
}
</script>

<template>
  <p v-if="status === 'ok'">Thanks - we will be in touch.</p>
  <form v-else action="https://forms.formward.eu/f/<FORM_ID>" method="post" @submit.prevent="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>
    <p v-if="status === 'error'" role="alert">Could not send. Try again.</p>
    <p>We only use your details to reply. <a href="/privacy">Privacy policy</a></p>
  </form>
</template>

Success and error handling

  • ref is auto-imported in Nuxt 3, so the component needs no explicit import - drop it into pages/ or components/ as-is.
  • Posting to Formward from the client means nuxi generate (full static export) keeps working; there is no Nitro route to render at request time.
  • The Accept: application/json header switches Formward from its 302-redirect default to the { ok, id, files } JSON response.

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 Nuxt submission

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