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

Handle Form Submissions in Astro Without a Backend

By Forminit in Guides • Published February 20, 2026

Short answer: Add the Forminit SDK to your Astro page, point your form at a Form ID, and submissions are stored, validated, and emailed to you automatically. No server adapter, no API routes, no serverless functions.


Astro builds static HTML by default. Static HTML can’t process form submissions — there’s no server to receive the POST request, no runtime to parse the data, no way to send an email. That means your contact form needs somewhere to send data to.

Forminit is a form backend API. You design the form in Astro, Forminit handles everything after submit: receiving data, server-side validation, storage, email notifications, file uploads, spam protection, and webhooks. Your Astro site stays static.

What you get

  • Email notifications on every submission — customizable templates, multiple recipients, Reply-to/CC/BCC
  • Submission dashboard — inbox-style UI with star, status tracking (open/in-progress/done/cancelled), internal notes, filtering, and search
  • Server-side validation — email format (RFC 5322), phone (E.164), URL, date (ISO 8601), rating (1-5), and country (ISO 3166-1) are validated before storage
  • File uploads — up to 25 MB per submission, 50+ MIME types (PDF, DOCX, images, video, archives), direct download URLs
  • Spam protection — reCAPTCHA v3, hCaptcha (visible/invisible), honeypot field
  • UTM and attribution tracking — auto-captures UTM parameters, ad click IDs (gclid, fbclid, msclkid, ttclid, twclid), referrer, and geolocation
  • Webhooks — forward submissions to any URL in real time (Slack, Discord, CRM, database, custom backend)
  • Zapier — connect to 5,000+ apps
  • CSV export — download submissions as a spreadsheet
  • Works on any host — Netlify, Vercel, Cloudflare Pages, GitHub Pages, AWS S3, or any static host

No server adapter needed. No environment variables. No email API accounts.

Setup

  1. Create a free account at forminit.com
  2. Create a form in the dashboard — set authentication to Public (no API key needed for client-side forms)
  3. Copy your Form ID

Install the SDK:

pnpm add forminit
# or: npm install forminit

Or skip npm and use the CDN — a 2 KB script tag:

<script src="https://forminit.com/sdk/v1/forminit.js"></script>

Basic contact form

The simplest working contact form on a static Astro page:

---
// src/pages/contact.astro
---

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

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

<script>
  import { Forminit } from 'forminit';

  const forminit = new Forminit();
  const FORM_ID = 'YOUR_FORM_ID';

  document.getElementById('contact-form')?.addEventListener('submit', async (e) => {
    e.preventDefault();
    const form = e.target as HTMLFormElement;
    const button = form.querySelector('button[type="submit"]') as HTMLButtonElement;
    const result = document.getElementById('form-result');

    button.disabled = true;
    button.textContent = 'Sending...';

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

    if (error) {
      if (result) result.textContent = error.message;
      button.disabled = false;
      button.textContent = 'Send';
      return;
    }

    if (result) result.textContent = 'Message sent!';
    form.reset();
    button.disabled = false;
    button.textContent = 'Send';
  });
</script>

That’s the entire implementation. Deploy your Astro site, and submissions appear in your Forminit dashboard with email notifications.

CDN version (no npm)

If you don’t want to install the SDK as a dependency, use the CDN script with is:inline:

---
// src/pages/contact.astro
---

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

<p id="form-result"></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 formData = new FormData(e.target);
    const { data, error } = await forminit.submit('YOUR_FORM_ID', formData);

    if (error) {
      document.getElementById('form-result').textContent = error.message;
      return;
    }

    document.getElementById('form-result').textContent = 'Message sent!';
    e.target.reset();
  });
</script>

Zero-JavaScript version (HTML form action)

For the simplest possible setup — no JavaScript at all — point the form’s action directly at Forminit:

---
// src/pages/contact.astro
---

<form action="https://forminit.com/f/YOUR_FORM_ID" method="POST">
  <input type="text" name="fi-sender-fullName" placeholder="Your name" required />
  <input type="email" name="fi-sender-email" placeholder="Your email" required />
  <textarea name="fi-text-message" placeholder="Your message" required></textarea>
  <button type="submit">Send</button>
</form>

The browser submits the form and redirects to a default thank-you page. No error handling, no AJAX, no UTM capture — but it works with zero code. Use this for the simplest static pages where you don’t need control over the submission flow.

What happens when a user submits

  1. The SDK sends a POST request to https://forminit.com/f/YOUR_FORM_ID with the form data
  2. Forminit validates typed fields server-side — bad emails, invalid phone numbers, and malformed URLs are rejected before storage
  3. The submission is stored in your dashboard
  4. Email notification is sent to you (and any other configured recipients)
  5. Webhooks, Slack, Discord, and Zapier integrations fire if configured
  6. The SDK returns { data, error } so you can show success/error states in your UI
  7. UTM parameters, ad click IDs, referrer, and geolocation are captured automatically by the SDK

Adding file uploads

Add a file input with the fi-file-{name} naming convention. The submission code stays the same — FormData handles file encoding automatically.

<form id="contact-form">
  <input type="text" name="fi-sender-fullName" placeholder="Your name" required />
  <input type="email" name="fi-sender-email" placeholder="Your email" required />
  <textarea name="fi-text-message" placeholder="Your message" required></textarea>

  <label for="attachment">Attachment (PDF, DOCX, images — up to 25 MB)</label>
  <input type="file" id="attachment" name="fi-file-attachment" accept=".pdf,.doc,.docx,.jpg,.png" />

  <button type="submit">Send</button>
</form>

For multiple files on a single input, append [] and add multiple:

<input type="file" name="fi-file-documents[]" multiple accept=".pdf,.doc,.docx" />

You can also combine multiple file inputs with different names:

<input type="file" name="fi-file-resume" accept=".pdf" />
<input type="file" name="fi-file-portfolio[]" accept="image/*" multiple />

The 25 MB limit applies to the combined total of all files in a single submission. Up to 20 file blocks per submission.

Adding spam protection

Honeypot (no third-party scripts)

Add a hidden field that bots fill but humans don’t see:

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

Enable honeypot in your Forminit dashboard under Form Settings > Security. Submissions with the honeypot field filled are silently rejected.

reCAPTCHA v3

<script is:inline src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<script>
  import { Forminit } from 'forminit';

  const forminit = new Forminit();

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

    const token = await grecaptcha.execute('YOUR_SITE_KEY', { action: 'submit' });

    const formData = new FormData(e.target as HTMLFormElement);
    formData.append('g-recaptcha-response', token);

    const { data, error } = await forminit.submit('YOUR_FORM_ID', formData);
    // handle response
  });
</script>

hCaptcha

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

Forminit validates the CAPTCHA token server-side. No additional backend code needed.

Reusable Astro component

If you have multiple forms across your site, extract the form into a component:

---
// src/components/ContactForm.astro
interface Props {
  formId: string;
  submitLabel?: string;
}

const { formId, submitLabel = 'Send' } = Astro.props;
---

<form class="forminit-form" data-form-id={formId}>
  <slot />
  <button type="submit">{submitLabel}</button>
</form>

<p class="forminit-result"></p>

<script>
  import { Forminit } from 'forminit';

  const forminit = new Forminit();

  document.querySelectorAll<HTMLFormElement>('.forminit-form').forEach((form) => {
    const formId = form.dataset.formId!;
    const result = form.nextElementSibling as HTMLElement;

    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const button = form.querySelector('button[type="submit"]') as HTMLButtonElement;

      button.disabled = true;
      button.textContent = 'Sending...';

      const formData = new FormData(form);
      const { data, error } = await forminit.submit(formId, formData);

      if (error) {
        result.textContent = error.message;
      } else {
        result.textContent = 'Message sent!';
        form.reset();
      }

      button.disabled = false;
      button.textContent = 'Send';
    });
  });
</script>

Use it anywhere:

---
import ContactForm from '../components/ContactForm.astro';
---

<ContactForm formId="YOUR_FORM_ID">
  <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>
</ContactForm>

Using React, Vue, or Svelte islands

If you’re using Astro islands with a UI framework, the SDK works the same way inside your components.

React example:

// src/components/ContactForm.tsx
import { useState } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit();

export default function ContactForm({ formId }: { formId: string }) {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [errorMsg, setErrorMsg] = useState('');

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

    const formData = new FormData(e.currentTarget);
    const { data, error } = await forminit.submit(formId, formData);

    if (error) {
      setStatus('error');
      setErrorMsg(error.message);
      return;
    }

    setStatus('success');
    (e.target as HTMLFormElement).reset();
  }

  return (
    <form onSubmit={handleSubmit}>
      <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 />
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
      {status === 'error' && <p>{errorMsg}</p>}
      {status === 'success' && <p>Message sent!</p>}
    </form>
  );
}
---
import ContactForm from '../components/ContactForm';
---

<ContactForm client:load formId="YOUR_FORM_ID" />

Field naming reference

Forminit uses a fi-{type}-{name} naming convention for form fields. Here are the most common fields for contact forms:

<!-- Contact info (max 1 sender block per form) -->
<input name="fi-sender-email" type="email" />
<input name="fi-sender-fullName" type="text" />
<input name="fi-sender-firstName" type="text" />
<input name="fi-sender-lastName" type="text" />
<input name="fi-sender-phone" type="tel" />
<input name="fi-sender-company" type="text" />

<!-- Text fields -->
<textarea name="fi-text-message"></textarea>
<input name="fi-text-subject" type="text" />

<!-- Other field types -->
<input name="fi-email-workEmail" type="email" />
<input name="fi-url-website" type="url" />
<input name="fi-phone-mobile" type="tel" />
<input name="fi-date-preferredDate" type="date" />
<input name="fi-number-budget" type="number" />
<select name="fi-select-department">...</select>
<input name="fi-rating-experience" type="number" min="1" max="5" />
<input name="fi-file-resume" type="file" />

Each field type has server-side validation. Emails are validated for RFC 5322 format, phone numbers for E.164 format, URLs for valid structure, dates for ISO 8601, and ratings for 1-5 range. Invalid data is rejected before storage.