Blog
4 min read

How to Add a Contact Form to Your Website (With or Without a Backend)

Four ways to make a contact form actually deliver messages — form services, your host's built-in forms, Google Forms, and your own backend route — plus how to stop spam and why you should never put an email API key in frontend code.

AI app builders are great at producing a beautiful contact form. What they often don't do is make it send anything. The button shows "Thanks!", and the message goes nowhere. Here are the ways to make a form actually reach your inbox, from easiest to most flexible.

Why a form needs something behind it

A form in the browser can collect text, but browsers can't send email by themselves. Something has to receive the submission and deliver it: a third-party service, your host, or your own server. (Background: frontend vs backend.)

Option 1: A form service (easiest, no backend)

Services like Formspree, Web3Forms, Basin and others give you a URL to send your form to. They email you each submission, and most have a free tier.

<form action="https://formspree.io/f/your-form-id" method="POST">
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

Some services give you a public "access key" designed to be in your HTML. That's fine — it only lets people submit to your form. It's different from a secret API key.

Good for: static sites, landing pages, portfolios. Watch: free-tier limits, and where the service stores your messages (it's personal data — see GDPR basics).

Option 2: Your host's built-in forms

Some static hosts handle forms for you. Netlify, for example, detects forms with a data-netlify="true" attribute and collects submissions in its dashboard. Easy if you're already there; it ties your form to that host.

Option 3: Google Forms or similar

Embed a Google Form (or Tally, Typeform…) on your page. Zero code, submissions in a spreadsheet. It won't match your design perfectly, but for a waitlist or feedback form it's fine.

Option 4: Your own backend route (most flexible)

If your app already has a server, add a route that receives the form and sends an email through an email provider's API:

// app/api/contact/route.ts (Next.js)
export async function POST(req: Request) {
  const { email, message } = await req.json();

  if (typeof email !== "string" || !email.includes("@") ||
      typeof message !== "string" || message.length > 5000) {
    return Response.json({ error: "Invalid input" }, { status: 400 });
  }

  await sendEmail({                    // your email provider's SDK
    to: "you@yourdomain.com",
    replyTo: email,
    subject: "New contact form message",
    text: message,
  });

  return Response.json({ ok: true });
}

The email provider's API key lives in a server-side environment variable. You control the validation, can save messages to your database, and can trigger anything else you like.

To make sure your emails actually arrive, set up SPF, DKIM and DMARC for your domain — see send email from your app without landing in spam.

The mistake to avoid

Never put an email-sending API key in frontend code. AI tools sometimes generate a form that calls an email API directly from the browser, with the key in a VITE_ or NEXT_PUBLIC_ variable. Anyone can open DevTools, copy the key, and send email as you — spam that gets your domain blacklisted. Keys belong on the server. See what is an API key?

Also avoid mailto:

<form action="mailto:you@example.com"> opens the visitor's email app instead of sending anything — if they even have one set up. And it exposes your address to scrapers. Use one of the options above.

Stopping spam

Any public form attracts bots within days. Layer these:

  1. A honeypot field — a hidden input real people never fill in. If it has a value, it's a bot:
    <input type="text" name="company_website" tabindex="-1" autocomplete="off" style="display:none">
    
  2. A CAPTCHA, such as Cloudflare Turnstile, which is free and usually invisible. See how to add a CAPTCHA to your forms.
  3. Rate limiting on your backend route — a few submissions per IP per hour. (What is rate limiting?)
  4. Validation on the server — length limits, a real-looking email. (Form validation explained)

Make the user experience clear

  • Show a clear success message only after the submission actually succeeds.
  • Show an error and keep what they typed if it fails.
  • Disable the button while sending to prevent double submissions.
  • Tell people how soon you'll reply.

The summary

  • A form needs something behind it to deliver messages.
  • Easiest: a form service or your host's built-in forms. Most flexible: your own backend route with an email API.
  • Never put email API keys in frontend code, and avoid mailto: forms.
  • Fight spam with a honeypot, a CAPTCHA, rate limiting and server-side validation.

EasySpawn runs your app with a real backend and server-side environment variables, so a contact form route — with validation, rate limiting and a database table for messages — is a few lines Claude Code can build and test. See how it works or join the waitlist.

Related: How to Get a Custom Email Address for Your Domain · Form Validation Explained · How to Launch Your First App · Static vs Dynamic Websites

Keep reading