Skip to main content
The usual Next.js answer to a contact form is an API route or a server action — which means your form now needs a server, which rules out output: 'export' static deployments and adds code you have to maintain. Pointing the form at a Crunchforms endpoint removes all of it: works with the App Router, the Pages Router, and fully static exports.

Before you start: create your endpoint

Every guide on this page needs a Crunchforms form ID. Getting one takes about two minutes:
  1. Sign up free — 1,000 submissions a month included, no credit card required.
  2. In your dashboard, click New Form, give it a name, and pick a response type: Redirect for classic form posts (the visitor lands on your thank-you page), or JSON if you’ll submit with JavaScript and handle the response yourself.
  3. Your endpoint is https://crunchforms.com/form/{formID} — copy it with the Copy Action URL button, or use the dashboard’s snippet generator.
The quickstart covers this in more detail.

Option 1: plain form post (works everywhere, zero JS)

A server component is enough. Set the form’s response type to Redirect and the visitor lands on your thank-you page:
app/contact/page.js
export const metadata = { title: 'Contact' };

export default function ContactPage() {
  return (
    <form action="https://crunchforms.com/form/{formID}" method="post">
      <label htmlFor="email">Email</label>
      <input name="email" type="email" id="email" required />

      <label htmlFor="message">Message</label>
      <textarea name="message" id="message" rows={5} required />

      <button type="submit">Send</button>
    </form>
  );
}
This ships no client JavaScript for the form at all.

Option 2: client component with a JSON response

To keep visitors on the page with inline feedback, make the form a client component and set the response type to JSON:
app/contact/ContactForm.js
'use client';

import { useState } from 'react';

export default function ContactForm() {
  const [status, setStatus] = useState('idle');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    const data = Object.fromEntries(new FormData(e.currentTarget));
    try {
      const res = await fetch('https://crunchforms.com/form/{formID}', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
      const result = await res.json();
      setStatus(result.success ? 'sent' : 'error');
    } catch {
      setStatus('error');
    }
  }

  if (status === 'sent') return <p>Thanks — we&apos;ll be in touch!</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required placeholder="you@example.com" />
      <textarea name="message" rows={5} required />
      <button type="submit" disabled={status === 'sending'}>
        {status === 'sending' ? 'Sending…' : 'Send'}
      </button>
      {status === 'error' && <p role="alert">Something went wrong — please try again.</p>}
    </form>
  );
}
Both options work under output: 'export' — no server, no API routes, no server actions. This documentation site’s parent product, crunchforms.com itself, is a static Next.js export using exactly this pattern.

Stop the spam before it starts

Every public form gets found by bots eventually. Crunchforms filters submissions server-side on every plan, including Free:

Honeypot

A hidden field humans never see — if it’s filled, the submission is rejected. Zero friction for real visitors.

Cloudflare Turnstile

Invisible bot detection. Add the widget to your form and Crunchforms validates the token server-side.

reCAPTCHA v3

Score-based validation with a threshold you control per form.
The honeypot alone stops most drive-by spam and takes one extra line of markup — enable it in your form’s settings, then include the hidden field in your form.

Troubleshooting

  • Check the form is set to Active in your dashboard.
  • If you’ve enabled Turnstile or reCAPTCHA on the form, submissions without a valid token are rejected — including your own tests.
  • Check you haven’t exceeded your plan’s monthly submission quota.
Set the form’s response type to Redirect and provide a success URL (and a failure URL) in the form settings. If you’re submitting with JavaScript, use the JSON response type instead and navigate in your code.
The endpoint accepts cross-origin requests, JSON bodies (Content-Type: application/json), and classic URL-encoded form bodies. Set the form’s response type to JSON so your code gets { "success": true } back instead of a redirect.
Use the Send Test Submission action next to your form in the dashboard — it posts a sample submission to your live endpoint so you can see the pipeline working end to end.

Next steps

Form configuration

Response types, notification emails, daily summaries, and more

Free HTML form templates

Contact, feedback, RSVP, job application, and quote-request forms ready to paste

Need help?

Get Support

Send us a note — we read everything.