integrations · framework

A Nuxt contact form that outlives nuxt generate.

Nuxt ships a server. Drop a file in server/api/, and Nitro gives you a real endpoint with no extra setup, which makes the obvious contact-form answer look easy. Then you run nuxt generate to prerender the site onto a CDN, and the obvious answer disappears: a fully static Nuxt build has no Nitro runtime, so server/api/contact.post.ts is compiled and then never deployed anywhere that can run it.

This is the trap that catches people who picked Nuxt precisely because it can go static. Point the form at a mailcontact endpoint and the contact page stops caring: it works identically under nuxt build with a Node server, under nuxt generate on a CDN, and on the preset your host swaps you onto next quarter.

the snippet

A working form for Nuxt, in one paste.

Point the form at your project's mailcontact endpoint. Submissions land in your project inbox. No server, no API route, no email service to wire up.

pages/contact.vue
<script setup>
// No server/api/contact.post.ts. Nothing here needs Nitro at runtime,
// so `nuxt generate` keeps this page fully working.
const sent = ref(false);

async function submit(event) {
  await $fetch("https://mailcontact.app/api/forms/your-form-id", {
    method: "POST",
    body: new FormData(event.target),
  });
  sent.value = true;
}
</script>

<template>
  <p v-if="sent">Thanks. We'll be in touch.</p>

  <form v-else @submit.prevent="submit">
    <input name="name" placeholder="Your name" required />
    <input name="email" type="email" placeholder="you@email.com" required />
    <textarea name="message" placeholder="What's on your mind?" required />
    <input name="_gotcha" tabindex="-1" autocomplete="off" hidden />

    <button>Send</button>
  </form>
</template>

how it works

Three steps, start to finish.

01

Create an inbox

One inbox per project. The form endpoint and the project email address are live before you add the page.

02

Add the page

Paste it into pages/contact.vue and swap the endpoint id. There is no server/ file to add, no runtimeConfig entry, and no private key to keep out of the client bundle.

03

Get notified

Add a Discord, Slack, email, or webhook channel, and submissions reach you the moment they arrive.

why mailcontact

Built to pair well with Nuxt.

Static output stays on the table

A Nitro route is the one thing that stops nuxt generate from being enough. Keep the form external and you keep the option to deploy the whole site as files, whenever you want it.

No mail key in runtimeConfig

The DIY version needs an API key for a mail provider, which means a secret, a place to store it, and a per-host config to get wrong. There is no key here to leak or rotate.

The preset stops mattering

Nitro presets differ per host, and a server route that works on one can behave differently on another. Markup pointing at an external URL behaves the same everywhere, because it never touches the preset.

faq

Nuxt + mailcontact, answered

Can I use a Nuxt server route instead?

Yes, if you deploy a server. server/api/contact.post.ts works well under nuxt build with a Node or serverless preset. It stops working under nuxt generate, because a prerendered site has no Nitro runtime to receive the POST. If static output matters to you, keep the form off the server graph.

Does this work with Nuxt 3 and Nuxt 4?

Yes. The example only uses ref, $fetch and @submit.prevent, all of which are stable across both. You can also swap $fetch for the native fetch: the payload is a FormData either way, so nothing here depends on Nuxt's fetch layer.

Should I use useFetch here?

No. useFetch is built for data you need during render, and it dedupes and caches, which is wrong for a one-shot POST triggered by a click. Call $fetch (or fetch) directly inside the submit handler.

How do I keep spam out without a captcha?

The hidden _gotcha field in the example is a honeypot: bots fill in every input they find, humans never see it, and a submission with it filled is dropped silently. It costs one line of markup and no user friction, unlike a challenge widget.

related

Also works with

One inbox for every Nuxt project you ship.

Forms and a real email address, together, with notifications where you live. Launching soon. Be the first to know.

No spam. It's kind of our thing.