Back to blog
    By

    Nuxt Contact Form Without a Server Route

    Add a contact form to a Nuxt 3 app that posts straight to a Formboost endpoint with $fetch — no server route, works on static generation and SSR.

    The reflex in Nuxt is to add server/api/contact.post.ts, then an email library, then credentials for it. None of that is needed for a contact form: the component can post directly to a Formboost endpoint, which stores the submission, screens it for spam and emails you. This works identically with nuxt generate (fully static), SSR, and edge deployments, because the request comes from the browser.

    Key takeaways

    • The component calls $fetch on the Formboost endpoint; there is no server/ directory involved.
    • It works on static hosting, where a Nitro handler would not run at all.
    • _replyto, _subject and _honey control the notification email and catch bots.
    • Keep the endpoint ID in runtimeConfig.public so it is one value per environment.

    1. Create the endpoint

    Create a form in the dashboard and copy its endpoint ID. Put it in nuxt.config.ts so components read it from config rather than a literal:

    1// nuxt.config.ts
    2export default defineNuxtConfig({
    3  runtimeConfig: {
    4    public: {
    5      formboostEndpoint: 'https://formboost.app/f/YOUR_ENDPOINT_ID',
    6    },
    7  },
    8});

    Override it per environment with NUXT_PUBLIC_FORMBOOST_ENDPOINT if you use a different form for staging.

    2. The component

    1<!-- components/ContactForm.vue -->
    2<template>
    3  <form @submit.prevent="submit">
    4    <label>
    5      Name
    6      <input v-model.trim="form.name" type="text" required />
    7    </label>
    8
    9    <label>
    10      Email
    11      <input v-model.trim="form._replyto" type="email" required />
    12    </label>
    13
    14    <label>
    15      Message
    16      <textarea v-model.trim="form.message" rows="5" required></textarea>
    17    </label>
    18
    19    <!-- Honeypot: hidden from people, filled by bots. Leave it empty. -->
    20    <input
    21      v-model="form._honey"
    22      type="text"
    23      name="_honey"
    24      style="display: none"
    25      tabindex="-1"
    26      autocomplete="off"
    27    />
    28
    29    <button type="submit" :disabled="pending">
    30      {{ pending ? 'Sending…' : 'Send message' }}
    31    </button>
    32
    33    <p v-if="sent" role="status">Thanks — we'll reply to {{ sentTo }} soon.</p>
    34    <p v-if="error" role="alert">{{ error }}</p>
    35  </form>
    36</template>
    37
    38<script setup lang="ts">
    39const { public: { formboostEndpoint } } = useRuntimeConfig();
    40
    41const form = reactive({
    42  name: '',
    43  _replyto: '',
    44  message: '',
    45  _honey: '',
    46  _subject: 'New message from the contact form',
    47});
    48
    49const pending = ref(false);
    50const sent = ref(false);
    51const error = ref('');
    52const sentTo = ref('');
    53
    54async function submit() {
    55  pending.value = true;
    56  error.value = '';
    57  sent.value = false;
    58
    59  try {
    60    // $fetch serialises the object as JSON and throws on any non-2xx status.
    61    await $fetch(formboostEndpoint, {
    62      method: 'POST',
    63      body: form,
    64      headers: { Accept: 'application/json' },
    65    });
    66    sentTo.value = form._replyto;
    67    sent.value = true;
    68    Object.assign(form, { name: '', _replyto: '', message: '', _honey: '' });
    69  } catch (err: any) {
    70    // Formboost's 4xx bodies carry a `message` hint and a machine-readable `name`.
    71    error.value = err?.data?.message || 'Something went wrong. Please try again.';
    72  } finally {
    73    pending.value = false;
    74  }
    75}
    76</script>

    Use it on a page:

    1<!-- pages/contact.vue -->
    2<template>
    3  <main>
    4    <h1>Contact</h1>
    5    <ContactForm />
    6  </main>
    7</template>

    Nuxt auto-imports ref, reactive, useRuntimeConfig and the component, so there are no imports to write. Formboost answers with 202 Accepted and a JSON body; the submission is stored and delivered in the background.

    3. Static, SSR or edge — it does not matter

    The request is made by the browser after hydration, so:

    • nuxt generate — the form works on Netlify, Vercel, Cloudflare Pages, GitHub Pages or an S3 bucket. There is no server to run.
    • SSR — same component, same request. Nothing on the server is involved.
    • Edge — same again. A Nitro route would have needed an email provider reachable from the edge runtime; the endpoint replaces that.

    The one thing you cannot do without a server route is keep a secret, and a contact form has none: the endpoint ID is public by design, the way a form's action URL always is. Spam is handled by screening every submission (heuristics on Free, an AI model from Starter), not by hiding the URL.

    4. Reserved fields

    FieldEffect
    _replytoReply-to on the notification email, so replying answers the visitor.
    _subjectSubject line of the notification email (up to 200 characters).
    _honeyThe honeypot. A filled honeypot marks the submission as spam; it is stored but not delivered.
    _redirectIgnored for a $fetch request. Only a plain HTML post is redirected.

    Everything else becomes a column in the dashboard and a key under submission in any webhook, Zapier or n8n payload.

    5. Progressive enhancement, if you want it

    A plain form inside the Nuxt template also works with JavaScript off — the browser posts it and Formboost answers a 302 to your _redirect URL (which must be https:// and on the same host as the page):

    1<template>
    2  <form :action="formboostEndpoint" method="POST">
    3    <input type="email" name="_replyto" required />
    4    <textarea name="message" required></textarea>
    5    <input type="hidden" name="_redirect" value="https://www.example.com/thanks" />
    6    <input type="text" name="_honey" style="display:none" tabindex="-1" autocomplete="off" />
    7    <button type="submit">Send</button>
    8  </form>
    9</template>
    10
    11<script setup lang="ts">
    12const { public: { formboostEndpoint } } = useRuntimeConfig();
    13</script>

    Where the submission goes next

    Email notification is on by default. Swap it for a Slack, Discord or Telegram message on the Free plan, or add a Google Sheet or a webhook on Starter. If a message does not arrive, the troubleshooting guide lists what to check, starting with the delivery log.

    Start with Formboost for free →