← All frameworks

React framework

Next.js form handling without a backend

You do not need an API route, a server action, or a database to handle a form in Next.js. The common pattern — write a route handler, parse the body, send an email, store the row — is exactly the work a form backend removes. Point your form at Formward instead and Next.js stays a pure frontend: no server to run, no inbox plumbing, no GDPR data store to manage.

The example below is a client component. It submits with fetch(), asks for a JSON response, and renders idle, success, and error states. It works the same in the App Router and the Pages Router because nothing about it is Next-specific beyond the React state — the submission goes straight to Formward's endpoint in Sweden.

EU-hosted · Your Next.js app can ship to any host while every name, email, and message it collects is received and stored in Sweden — GDPR-clean, no EU-US transfer to account for.

The Next.js example

Copy this into your project and replace <FORM_ID> with the id of a form you create in the Formward dashboard.

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;

    try {
      const res = await fetch("https://forms.formward.eu/f/<FORM_ID>", {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(form),
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json(); // { ok: true, id, files }
      if (!data.ok) throw new Error("Submission rejected");
      form.reset();
      setStatus("ok");
    } catch {
      setStatus("error");
    }
  }

  if (status === "ok") return <p>Thankswe will be in touch.</p>;

  return (
    <form onSubmit={onSubmit}>
      <input type="email" name="email" placeholder="you@example.com" required />
      <textarea name="message" placeholder="Your message" required />
      {/* Honeypot: real users never see or fill this. */}
      <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off"
        style={{ position: "absolute", left: "-5000px" }} aria-hidden />
      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending…" : "Send"}
      </button>
      {status === "error" && <p role="alert">Something went wrong. Please try again.</p>}
    </form>
  );
}

Success and error handling

  • Sending the Accept: application/json header tells Formward to return { ok: true, id, files } as JSON instead of issuing a 302 redirect, so the user stays on the page.
  • A non-2xx status (e.g. 422 validation, 429 plan limit) lands in the catch block — read res.json() for an { ok: false, error } body if you want to show the server's message.
  • Keep the hidden _gotcha input empty; Formward silently drops any submission where it is filled, which stops most bots without a CAPTCHA.

Spam protection

Every example above includes the _gotcha honeypot field. It is hidden from real users and must stay empty; Formward silently drops any submission where it is filled, which stops most bots with no CAPTCHA. For a stricter gate, add a Cloudflare Turnstile widget and send its token as the cf-turnstile-response field — Formward verifies it on receipt.

The JSON response

A standard POST gets a 302 redirect (or your thank-you page). Send the Accept: application/json header — as every fetch() example above does — and Formward returns JSON instead:

HTTP/1.1 200 OK
Content-Type: application/json

{ "ok": true, "id": "clxyz123...", "files": [] }

On failure the body is { ok: false, error } with a 4xx status (validation, plan limit, and so on). See the AJAX docs for the full status-code table and CORS notes.

Other frameworks

Collect your first Next.js submission

Create a form, paste the snippet, and keep every submission in the EU. GDPR-clean from the first POST.

Next.js form handling without a backend | Formward