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>Thanks — we 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.