Skip to content

Next.js forms

Formkove works with Next.js without an API route or a server action. The browser posts straight to your form’s endpoint.

Before you start: add your site’s origin (e.g. https://example.com, and http://localhost:3000 for local development) under your form’s Settings → Allowed Origins & Redirects. Browsers block fetch() responses from sites that aren’t listed. See Allowed Origins.

app/contact/contact-form.tsx
"use client";
import { useState, type FormEvent } from "react";
const ENDPOINT = `https://app.formkove.com/api/forms/${process.env.NEXT_PUBLIC_FORMKOVE_FORM_ID}/submissions`;
export function ContactForm() {
const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
const [error, setError] = useState("");
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = event.currentTarget;
setStatus("sending");
try {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error || "Something went wrong.");
form.reset();
setStatus("sent");
} catch (err) {
setError(err instanceof Error ? err.message : "Network error. Please try again.");
setStatus("error");
}
}
if (status === "sent") return <p>Thanks! We'll be in touch.</p>;
return (
<form onSubmit={onSubmit}>
<input name="name" placeholder="Your name" required />
<input name="email" type="email" placeholder="Email address" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={status === "sending"}>
{status === "sending" ? "Sending..." : "Send"}
</button>
{status === "error" && <p role="alert">{error}</p>}
</form>
);
}

Use it from any page:

app/contact/page.tsx
import { ContactForm } from "./contact-form";
export default function ContactPage() {
return <ContactForm />;
}

Set the form ID in .env.local:

Terminal window
NEXT_PUBLIC_FORMKOVE_FORM_ID=form1_your_form_id

The form ID is safe to expose. It can only send submissions to its own form.

The same component works in pages/contact.tsx. Remove the "use client" line.

A plain HTML form also works in Next.js and needs no Allowed Origins entry for the submission itself:

<form
action={`https://app.formkove.com/api/forms/${process.env.NEXT_PUBLIC_FORMKOVE_FORM_ID}/submissions?redirect=https://example.com/thanks`}
method="POST"
>
{/* fields */}
</form>

The redirect target must be listed under Allowed Origins. See Success pages and redirects.

You can post from a server action or route handler, but every submission then comes from your server’s IP address. Formkove rate limits per IP and form, so all your visitors would share one limit. Submitting from the browser keeps the limit per visitor.