FormThunder isn't open yet. This site describes the service as it will run at launch. Find out more

FormThunder
Menu

Build a Next.js contact form with one client component

In the Next.js App Router, a contact form needs one client component and no API route. It posts straight from the browser to FormThunder, which stores the submission and emails you. The page around it stays a server component. A browser post also keeps the visitor's IP and origin, so rate limits and domain restriction work per visitor.

Updated

The Next.js code

app/contact/contact-form.tsx
"use client";

import { useState, type FormEvent } from "react";

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    setStatus("sending");
    const response = await fetch("https://formthunder.com/submit", {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify({
        ...Object.fromEntries(new FormData(form)),
        access_key: process.env.NEXT_PUBLIC_FORMTHUNDER_KEY,
      }),
    }).catch(() => null);
    const ok = response ? (await response.json()).success : false;
    setStatus(ok ? "sent" : "error");
    if (ok) form.reset();
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <button disabled={status === "sending"}>Send</button>
      {status === "sent" && <p role="status">Thanks, we got it.</p>}
      {status === "error" && <p role="alert">That didn't send. Please try again.</p>}
    </form>
  );
}
app/contact/page.tsx
import { ContactForm } from "./contact-form";

export const metadata = { title: "Contact" };

export default function ContactPage() {
  return (
    <main>
      <h1>Contact us</h1>
      <ContactForm />
    </main>
  );
}

Replace YOUR_ACCESS_KEY with your free key. It's public by design, so it's safe in client code.

Notes for Next.js

  • Only NEXT_PUBLIC_ variables reach the browser. NEXT_PUBLIC_FORMTHUNDER_KEY is inlined at build time, so redeploy after changing it.
  • Avoid posting to /submit from a server action or route handler. Every visitor would share your server's IP for rate limiting, and with no Origin header domain restriction refuses the request.
  • If you do need to send from the server, use POST /api/v1/submissions with an API key kept in a server-only variable. That route is on the Pro plan and skips the public checks.
  • Pages Router works the same way: put the component in components/ and render it from pages/contact.tsx, without "use client".

Or have your AI add it

Connect the MCP server, then ask. The agent creates the form and writes the code for you, and it can send a real test when it's done.

You, to your AI
Add a FormThunder contact form to this Next.js site that emails hello@example.com. Send a test when you're done.

Questions

Do I need an API route in Next.js for a contact form?

No. The access key is public by design, so the browser can post straight to FormThunder.

Will this work with static export?

Yes. The form runs entirely in the browser, so output: "export" is fine.

Get your Next.js form sending

The free plan covers 250 submissions a month, with MCP, the CLI and the brand kit included.