New: Getform is now Forminit. Read the post ->
Back to all posts

Form Backend for Static Sites: Complete Guide

By Forminit in Guides • Published February 20, 2026

Short answer: Use a form backend API. Your static site submits form data to the backend’s endpoint. The backend validates, stores, and sends notifications. No server, no serverless functions, no database. Works on any static host.


Static sites don’t have a backend. That’s the point — they’re fast, cheap, secure, and simple to deploy. But the moment you need a contact form, feedback form, or any kind of data collection, you need a server to receive it.

Except you don’t. A form backend API handles the server side so your site stays static.

The static site form problem

Static site generators (Astro, Hugo, Jekyll, Eleventy, Next.js static export, Gatsby) output HTML, CSS, and JavaScript files. There’s no server to process POST requests. The same is true for any site hosted on Netlify, Vercel, Cloudflare Pages, GitHub Pages, or a CDN.

Your options:

  1. Add a server — Defeats the purpose of a static site. Now you’re managing infrastructure.
  2. Use serverless functions — Adds complexity. You need a function, an email API, possibly a database, and you’re managing 2-4 services for a contact form.
  3. Use the hosting platform’s built-in forms — Locks you to that platform (Netlify Forms only works on Netlify).
  4. Use a form backend API — A single service handles everything. Works with any host.

Option 4 is what this guide covers.

How a form backend works with static sites

Your static site has an HTML form. When the user submits, JavaScript sends the form data to the backend’s API endpoint. The backend processes it and returns a response. Your JavaScript handles the response (show success message, redirect, display errors).

User fills form → JavaScript sends POST → Form backend API → validates, stores, notifies

                                         Response to browser
                                         (success or error)

The form backend runs on its own infrastructure. Your static site never needs a server.

Setting it up with Forminit

Step 1: Create a form

Sign up at forminit.com and create a form in the dashboard. You’ll get a form ID.

Step 2: Add the form to your HTML

<form id="contact-form">
  <input type="text" name="fi-sender-fullName" placeholder="Name" required />
  <input type="email" name="fi-sender-email" placeholder="Email" required />
  <textarea name="fi-text-message" placeholder="Message" required></textarea>
  <button type="submit">Send</button>
</form>

<p id="form-status"></p>

Step 3: Add the SDK and submit handler

<script src="https://forminit.com/sdk/v1/forminit.js"></script>
<script>
  const forminit = new Forminit();
  const FORM_ID = 'YOUR_FORM_ID';

  document.getElementById('contact-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const status = document.getElementById('form-status');

    status.textContent = 'Sending...';

    const { data, error } = await forminit.submit(FORM_ID, new FormData(e.target));

    if (error) {
      status.textContent = error.message;
      return;
    }

    status.textContent = 'Message sent!';
    e.target.reset();
  });
</script>

That’s it. Deploy your static site to any host. The form works.

What happens automatically

Without any extra code, the SDK and backend handle:

  • Server-side validation — Email format checked, phone numbers validated, file types verified
  • Submission storage — Every submission is saved and viewable in the dashboard
  • Email notification — You get an email when someone submits (configurable)
  • UTM tracking — If the page URL has UTM parameters, they’re captured automatically
  • Ad click IDs — gclid, fbclid, msclkid, and others are captured from the URL
  • Referrer — Where the user came from
  • Geolocation — Country, city, timezone from IP

Framework-specific examples

Astro

---
// src/pages/contact.astro
---
<html>
<body>
  <form id="contact-form">
    <input type="text" name="fi-sender-fullName" placeholder="Name" required />
    <input type="email" name="fi-sender-email" placeholder="Email" required />
    <textarea name="fi-text-message" placeholder="Message" required></textarea>
    <button type="submit">Send</button>
  </form>

  <p id="status"></p>

  <script is:inline src="https://forminit.com/sdk/v1/forminit.js"></script>
  <script is:inline>
    const forminit = new Forminit();
    document.getElementById('contact-form').addEventListener('submit', async (e) => {
      e.preventDefault();
      const { error } = await forminit.submit('YOUR_FORM_ID', new FormData(e.target));
      document.getElementById('status').textContent = error ? error.message : 'Sent!';
      if (!error) e.target.reset();
    });
  </script>
</body>
</html>

Hugo

Hugo outputs plain HTML. Add the form to any template or page:

<!-- layouts/page/contact.html or content as raw HTML -->
<form id="contact-form">
  <input type="text" name="fi-sender-fullName" placeholder="Name" required />
  <input type="email" name="fi-sender-email" placeholder="Email" required />
  <textarea name="fi-text-message" placeholder="Message" required></textarea>
  <button type="submit">Send</button>
</form>

<p id="status"></p>

<script src="https://forminit.com/sdk/v1/forminit.js"></script>
<script>
  const forminit = new Forminit();
  document.getElementById('contact-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const { error } = await forminit.submit('YOUR_FORM_ID', new FormData(e.target));
    document.getElementById('status').textContent = error ? error.message : 'Sent!';
    if (!error) e.target.reset();
  });
</script>

Jekyll

Same approach. Add to any layout or include:

<!-- _includes/contact-form.html -->
<form id="contact-form">
  <input type="text" name="fi-sender-fullName" placeholder="Name" required />
  <input type="email" name="fi-sender-email" placeholder="Email" required />
  <textarea name="fi-text-message" placeholder="Message" required></textarea>
  <button type="submit">Send</button>
</form>

<p id="status"></p>

<script src="https://forminit.com/sdk/v1/forminit.js"></script>
<script>
  const forminit = new Forminit();
  document.getElementById('contact-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const { error } = await forminit.submit('YOUR_FORM_ID', new FormData(e.target));
    document.getElementById('status').textContent = error ? error.message : 'Sent!';
    if (!error) e.target.reset();
  });
</script>

Eleventy (11ty)

Eleventy generates static HTML. The form code is the same plain HTML + SDK pattern.

Next.js (static export)

If you’re using output: 'export' in Next.js for a fully static site:

'use client';
import { useState } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit();

export function ContactForm() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('loading');

    const { error } = await forminit.submit('YOUR_FORM_ID', new FormData(e.currentTarget));
    setStatus(error ? 'error' : 'success');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="fi-sender-fullName" placeholder="Name" required />
      <input name="fi-sender-email" type="email" placeholder="Email" required />
      <textarea name="fi-text-message" placeholder="Message" required />
      <button disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
      {status === 'success' && <p>Sent!</p>}
      {status === 'error' && <p>Something went wrong.</p>}
    </form>
  );
}

Hosting compatibility

Form backends work with every static host because they’re just API calls from the browser. There’s nothing to configure on the hosting side.

HostWorks with form backend?Notes
NetlifyYesAlso has Netlify Forms (platform-locked)
VercelYes
Cloudflare PagesYes
GitHub PagesYes
AWS S3 + CloudFrontYes
Firebase HostingYes
RenderYes
DigitalOcean App PlatformYes
Any CDN or web serverYes

The point: your form backend is decoupled from your hosting. Move between hosts freely without changing your form setup.

Adding file uploads

Static sites can handle file uploads through the form backend without any server-side file processing.

<form id="upload-form">
  <input type="text" name="fi-sender-fullName" placeholder="Name" required />
  <input type="email" name="fi-sender-email" placeholder="Email" required />
  <textarea name="fi-text-message" placeholder="Message" required></textarea>
  <input type="file" name="fi-file-attachment" accept=".pdf,.doc,.docx" />
  <button type="submit">Send</button>
</form>

The SDK sends the file as multipart/form-data automatically when you pass a FormData object. Forminit stores the file and provides a download URL in the dashboard. Up to 25 MB per submission, 50+ supported file types.

For a detailed file upload tutorial, see How to Add File Uploads to Your Contact Form Without a Server.

Adding spam protection

Honeypot (simplest)

Add a hidden field that real users won’t fill in, but bots will:

<input type="text" name="fi-hp" style="display:none" tabindex="-1" autocomplete="off" />

Enable honeypot protection in Form Settings > Security in the Forminit dashboard.

reCAPTCHA v3 (invisible)

<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<script>
  // Before submitting, get token
  const token = await grecaptcha.execute('YOUR_SITE_KEY', { action: 'submit' });
  formData.append('g-recaptcha-response', token);
</script>

hCaptcha

<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>

Why not just use Netlify Forms?

If you’re on Netlify, their built-in forms work with zero config. The trade-off:

Netlify FormsForm backend (Forminit)
Works on NetlifyYesYes
Works on Vercel, Cloudflare, etc.NoYes
Server-side validationNoYes (typed blocks)
File uploads10 MB25 MB
WebhooksPaid onlyAll plans
UTM trackingNoAutomatic
DashboardBasic tableInbox with status, notes

If you’re committed to Netlify forever, their forms are convenient. If there’s any chance you’ll switch hosts, or if you need validation, larger file uploads, or webhooks on a free/starter plan, a standalone form backend is the better choice.

Further reading