Skip to content
FFormhook
All guides

Guide

Nuxt contact form (no backend needed)

Nuxt doesn't need a server route to receive contact form submissions. Drop the plain HTML form into a Nuxt page, or — if you want client-side feedback — use a Vue 3 <script setup> handler with $fetch and a ref to track the submission status.

Use a plain HTML form

You do not need an API route. The form POSTs directly to Formhook, and your framework never has to touch the submission.

contact.html
<form action="https://formhook.app/f/YOUR_API_KEY" method="POST">
  <label>
    Email
    <input type="email" name="email" required />
  </label>

  <label>
    Message
    <textarea name="message" required></textarea>
  </label>

  <!-- honeypot: bots fill this; humans don't see it -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" hidden />

  <!-- optional: send users to a thank-you page after a native POST -->
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />

  <button type="submit">Send</button>
</form>

Or submit with fetch in Nuxt

Use this version if you want client-side success/error states without a full page reload.

pages/contact.vue
<!-- pages/contact.vue -->
<script setup lang="ts">
type Status = "idle" | "sending" | "ok" | "error";
const status = ref<Status>("idle");

async function onSubmit(e: Event) {
  e.preventDefault();
  status.value = "sending";

  const form = e.target as HTMLFormElement;
  const data = Object.fromEntries(new FormData(form));

  try {
    await $fetch("https://formhook.app/f/YOUR_API_KEY", {
      method: "POST",
      body: data,
    });
    status.value = "ok";
    form.reset();
  } catch {
    status.value = "error";
  }
}
</script>

<template>
  <p v-if="status === 'ok'">Thanks — we'll be in touch.</p>
  <form v-else @submit="onSubmit">
    <input type="email" name="email" placeholder="Email" required />
    <textarea name="message" placeholder="Message" required />
    <input type="text" name="_gotcha" tabindex="-1" hidden />
    <button type="submit" :disabled="status === 'sending'">
      {{ status === "sending" ? "Sending…" : "Send" }}
    </button>
    <p v-if="status === 'error'">Something went wrong. Please try again.</p>
  </form>
</template>

How it works

The plain form snippet works inside any Nuxt page (pages/contact.vue) without a corresponding server/api route. For client-side success/error states, the second snippet uses $fetch — Nuxt's built-in fetch wrapper — and a ref to drive the status state machine. Both go straight to formhook.app/f/YOUR_API_KEY.

What happens next

  • Submission lands in your Formhook dashboard within a second.
  • Push notification fires on every device you've enabled it on.
  • Reply directly from the dashboard (Pro and above) — no Nuxt server API or transactional email integration required.

Ship your Nuxt contact form today.

Sign up free, create a form, paste the API key. Five minutes.

Start free

Free tier: 5 forms · 250 submissions/month · No credit card.

Nuxt contact form — FAQ

Do I need to write a Nitro server route for the form?
No. The form posts to Formhook directly, which means your Nuxt site can stay fully static if that's what you want. No /server/api/contact.ts to maintain.
Does this work with `nuxt generate` (static site)?
Yes. The contact form is just an HTML POST — it doesn't care whether the rest of your Nuxt site is rendered statically or on the edge.
Where do I put the API key? Is it safe to commit?
The API key is a per-form identifier, not a secret in the traditional sense — it's already exposed in every visitor's HTML. Treat it as a public endpoint identifier and rely on Formhook's per-form rate limits, CORS allowlist, and Turnstile toggle for protection.

Guides for other frameworks