All resources
Static site forms

Vue Contact Form Without a Backend (Composition API)

A working Vue 3 contact form with no server and no API route: a plain-action version and a Composition API version with loading and success states. Nuxt notes included.

The ShipMyForm team

· 2 min read

A Vue contact form doesn't need a server. The form POSTs to a hosted endpoint; the service behind it stores the submission, screens the spam, and emails you. Your Vue app stays a static bundle — Vite build, any CDN, no API route, no nodemailer, no credentials in client code.

Two patterns cover it, both against the same endpoint.

Pattern 1: plain action, zero script

If the contact page just needs to work, HTML already does this:

vue
<template>
  <form action="https://shipmyform.com/f/YOUR_FORM_ID" method="POST">
    <input type="text" name="name" placeholder="Your name" required />
    <input type="email" name="email" placeholder="[email protected]" required />
    <textarea name="message" placeholder="How can we help?" required></textarea>
    <!-- honeypot: hidden from humans, filled by bots -->
    <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none" />
    <input type="hidden" name="_redirect" value="https://yoursite.com/thanks" />
    <button type="submit">Send</button>
  </form>
</template>

No script block at all. Field names become the fields you receive; _redirect returns the visitor to your thank-you page.

Pattern 2: Composition API with status states

To stay on the page with a disabled button and inline confirmation:

vue
<script setup>
import { ref } from "vue";

const status = ref("idle"); // idle | sending | sent | error

async function submit(e) {
  status.value = "sending";
  try {
    const res = await fetch("https://shipmyform.com/f/YOUR_FORM_ID", {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(e.target),
    });
    status.value = res.ok ? "sent" : "error";
  } catch {
    status.value = "error";
  }
}
</script>

<template>
  <p v-if="status === 'sent'">Thanks — we'll get back to you soon.</p>
  <form v-else @submit.prevent="submit">
    <input type="text" name="name" placeholder="Your name" required />
    <input type="email" name="email" placeholder="[email protected]" required />
    <textarea name="message" placeholder="How can we help?" required></textarea>
    <button type="submit" :disabled="status === 'sending'">
      {{ status === "sending" ? "Sending…" : "Send" }}
    </button>
    <p v-if="status === 'error'" role="alert">Something went wrong — try again.</p>
  </form>
</template>

Notice what's absent: no v-model per field, no reactive form object. new FormData(e.target) reads the DOM at submit time — v-model earns its place only when something must react while the user types. The endpoint returns JSON ({ ok: true }) because of the Accept header; error handling in more depth lives in submit a form with fetch.

Nuxt, and the "just add a server route" question

On a static or SPA Nuxt site, the pattern above is the answer as-is. If you run the Nuxt server, you could proxy the form through a server route — but for a contact form it usually adds moving parts without capability: sending email still needs authenticated infrastructure, spam still needs screening, submissions still need to live somewhere. That's the whole job of a form backend, and as of 2026 ShipMyForm's free plan does it for 100 submissions a month — spam pipeline, searchable inbox, Slack/Sheets/webhook routing — with Vue-specific docs for the details.

The one rule: no email credentials in the bundle:

Browser-side "email sending" packages embed a token that ships to every visitor — including the ones running scrapers. If a Vue tutorial puts an API key in a component, that key is public. POST to an endpoint; let secrets live server-side.

Next steps

Frequently asked questions

How do I make a contact form in Vue without a backend?
POST the form to a hosted form endpoint instead of your own server. Either set the form's action attribute (works with zero JavaScript) or submit a FormData body with fetch inside your setup function for inline loading and success states. The endpoint stores the submission, filters spam, and emails you.
How do I send Vue form data to my email?
Through a form backend: your Vue app POSTs the fields, and the backend delivers them to your inbox from an authenticated sending domain. Avoid any package that sends email directly from the browser — its credentials ship in your bundle where anyone can extract and abuse them.
Do I need v-model on every field?
Not for submission. new FormData(formElement) collects values from any input with a name attribute at submit time, no reactive state required. Use v-model where you want live behavior — validation as the user types, character counters — and skip it everywhere else.
Does this work with Nuxt?
Yes. In a statically generated or SPA Nuxt site, the endpoint pattern is the standard answer. If you're running the Nuxt server anyway, you could proxy through a server route instead — but for a contact form that adds moving parts without adding capability, since spam filtering and delivery still have to live somewhere.
How is spam handled without my own server?
At the endpoint, before anything reaches you: honeypot checks, rate limiting, machine-learning classification, and a closer review of ambiguous messages. Server-side screening matters because bots POST directly to endpoints — client-side validation never slows them down.

Related guides