How to handle form submissions in Next.js
Short answer: Post the form to your backend endpoint either from a server action (keeps the endpoint server-side), a client component with fetch(), or a plain HTML form. The endpoint is the same in all three cases.
Next.js gives you several ways to submit a form, and the choice mostly affects where the request is sent from, not what the backend sees.
Pattern 1: Plain HTML form (works in any deployment)
export default function ContactPage() {
return (
<form action="https://formivo.faizyab.me/api/f/YOUR_FORM_ID" method="POST">
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<button type="submit">Send</button>
</form>
);
}No JavaScript, no hydration cost, and it survives a client-side crash. The trade-off is a navigation to the confirmation page.
Pattern 2: Client component with fetch()
"use client";
import { useState } from "react";
export function ContactForm() {
const [state, setState] = useState<"idle" | "sending" | "sent" | "error">("idle");
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = event.currentTarget;
setState("sending");
const response = await fetch("https://formivo.faizyab.me/api/f/YOUR_FORM_ID", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});
setState(response.ok ? "sent" : "error");
if (response.ok) form.reset();
}
return (
<form onSubmit={onSubmit}>
<input name="email" type="email" required aria-label="Email" />
<button disabled={state === "sending"}>
{state === "sending" ? "Sending…" : state === "sent" ? "Sent" : "Send"}
</button>
{state === "error" && <p role="alert">Could not send. Please try again.</p>}
</form>
);
}Pattern 3: Server action
"use server";
export async function submitContact(_prev: unknown, formData: FormData) {
const response = await fetch(
`https://formivo.faizyab.me/api/f/${process.env.NEXT_PUBLIC_CONTACT_FORM_ID}`,
{
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(Object.fromEntries(formData)),
},
);
if (!response.ok) return { error: "Could not send. Please try again." };
return { sent: true };
}A server action keeps the endpoint out of the client bundle and lets you add logic before forwarding. Remember that anything in a `NEXT_PUBLIC_` variable is public: a form id is fine, an API key is not.
Where should the submissions live?
| Approach | You own | You maintain |
|---|---|---|
| Your own database + email | Everything | Schema, spam, deliverability, backup |
| Hosted form backend | The form UI | The form UI |
| Marketing automation tool | The form UI | Vendor limits and pricing |
Which pattern to pick
- 1Marketing or docs page: plain HTML form. Zero JavaScript, zero hydration.
- 2Dashboard or app form: client fetch with proper error and loading states.
- 3You need to enrich the payload server-side (UTMs, user id): server action.
All three send the same request shape, so you can move between them without touching the backend or its data.