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
"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>
);
}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_KEYis inlined at build time, so redeploy after changing it. - Avoid posting to
/submitfrom a server action or route handler. Every visitor would share your server's IP for rate limiting, and with noOriginheader domain restriction refuses the request. - If you do need to send from the server, use
POST /api/v1/submissionswith 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 frompages/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.
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.