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
$fetchon the Formboost endpoint; there is noserver/directory involved.- It works on static hosting, where a Nitro handler would not run at all.
_replyto,_subjectand_honeycontrol the notification email and catch bots.- Keep the endpoint ID in
runtimeConfig.publicso 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
| Field | Effect |
|---|---|
_replyto | Reply-to on the notification email, so replying answers the visitor. |
_subject | Subject line of the notification email (up to 200 characters). |
_honey | The honeypot. A filled honeypot marks the submission as spam; it is stored but not delivered. |
_redirect | Ignored 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.