Form Backend for Static Sites: Complete Guide
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:
- Add a server — Defeats the purpose of a static site. Now you’re managing infrastructure.
- 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.
- Use the hosting platform’s built-in forms — Locks you to that platform (Netlify Forms only works on Netlify).
- 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.
| Host | Works with form backend? | Notes |
|---|---|---|
| Netlify | Yes | Also has Netlify Forms (platform-locked) |
| Vercel | Yes | — |
| Cloudflare Pages | Yes | — |
| GitHub Pages | Yes | — |
| AWS S3 + CloudFront | Yes | — |
| Firebase Hosting | Yes | — |
| Render | Yes | — |
| DigitalOcean App Platform | Yes | — |
| Any CDN or web server | Yes | — |
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 Forms | Form backend (Forminit) | |
|---|---|---|
| Works on Netlify | Yes | Yes |
| Works on Vercel, Cloudflare, etc. | No | Yes |
| Server-side validation | No | Yes (typed blocks) |
| File uploads | 10 MB | 25 MB |
| Webhooks | Paid only | All plans |
| UTM tracking | No | Automatic |
| Dashboard | Basic table | Inbox 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.