Skip to content
FFormhook
All guides

Guide

Next.js contact form (no backend needed)

Next.js doesn't need its own form receiver. You can drop the same plain HTML form action into any Server Component, or — if you want client-side success and error states — use a small Client Component with fetch. Both POST directly to Formhook.

Use a plain HTML form

You do not need an API route. The form POSTs directly to Formhook, and your framework never has to touch the submission.

contact.html
<form action="https://formhook.app/f/YOUR_API_KEY" method="POST">
  <label>
    Email
    <input type="email" name="email" required />
  </label>

  <label>
    Message
    <textarea name="message" required></textarea>
  </label>

  <!-- honeypot: bots fill this; humans don't see it -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" hidden />

  <!-- optional: send users to a thank-you page after a native POST -->
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />

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

Or submit with fetch in Next.js

Use this version if you want client-side success/error states without a full page reload.

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

import { useState } from "react";

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

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("sending");

    const form = e.currentTarget;
    const data = Object.fromEntries(new FormData(form));

    const res = await fetch("https://formhook.app/f/YOUR_API_KEY", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });

    setStatus(res.ok ? "ok" : "error");
    if (res.ok) form.reset();
  }

  if (status === "ok") return <p>Thanks — we'll be in touch.</p>;

  return (
    <form onSubmit={onSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <input type="text" name="_gotcha" tabIndex={-1} hidden />
      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending…" : "Send"}
      </button>
      {status === "error" && <p>Something went wrong. Please try again.</p>}
    </form>
  );
}

How it works

The plain form action works inside any Next.js page or layout, including Server Components. No app/api/contact/route.ts, no Server Action, no middleware. If you want a client-side UX with success and error states, the second snippet shows the canonical pattern: "use client", useState, onSubmit, fetch to formhook.app/f/YOUR_API_KEY. The honeypot (_gotcha) and optional _redirect are the same special fields from the HTML guide.

What happens next

  • Submission appears in your Formhook dashboard within a second.
  • Push notification fires on every device you've enabled it on.
  • Reply directly from the dashboard (Pro and above) — handy for client work where each project has its own contact route.

Ship your Next.js contact form today.

Sign up free, create a form, paste the API key. Five minutes.

Start free

Free tier: 5 forms · 250 submissions/month · No credit card.

Next.js contact form — FAQ

Do I need to add an API route or a Server Action?
No. The form POSTs to Formhook directly. Skipping the API route also means one less surface to rate-limit yourself, and one less cold start to pay for on serverless deployments.
Can I use this in a Server Component?
Yes — the plain HTML form snippet works in any Server Component. Only the fetch-based version needs to be a Client Component, because it uses useState.
How do I handle spam without writing rate-limit middleware?
Spam protection is on Formhook's side. The honeypot drops bots silently, and Cloudflare Turnstile is a per-form toggle in the dashboard. Per-IP and per-form rate limits are also enforced server-side.

Guides for other frameworks