How to Handle Form Submissions in Next.js Without Writing Backend Code
You've got a Next.js site with a contact form, a feedback form, or a lead capture. The conventional approach is to write an API route in app/api/submit/route.ts, set up a database, configure email delivery, and handle spam. For one form, that's a lot of plumbing.
Instead, you can skip all of that and point your form at a managed form submission API. The API handles storage, spam filtering, and email notifications — you just build the frontend.
The Traditional Next.js Form Approach
Here's what you'd typically build for a simple contact form in Next.js:
- A React form component with state management
- An API route (
/api/submit) to receive the data - Validation logic (server-side)
- A database or storage solution for submissions
- Email sending via Resend, SendGrid, or Nodemailer
- Spam protection (reCAPTCHA, honeypot fields)
- Rate limiting to prevent abuse
That's 5–7 different concerns for a single form. For a small business site or portfolio, this is massive overkill. Even for larger projects, it's often better to delegate this to a service.
The Simpler Approach: Form Submission API
With a form submission API, your architecture looks like this:
No API routes. No database. No email configuration. You build the form, the service handles everything else.
Implementation
The Form Component
Here's a complete contact form component for Next.js using a form submission API:
"use client";
import { useState, FormEvent } from "react";
export default function ContactForm() {
const [status, setStatus] = useState<
"idle" | "submitting" | "success" | "error"
>("idle");
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("submitting");
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData);
try {
const res = await fetch(
"https://edgesubmit.com/api/submit/YOUR_FORM_ID",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
);
if (!res.ok) throw new Error("Failed");
setStatus("success");
e.currentTarget.reset();
} catch {
setStatus("error");
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-lg">
<div>
<label htmlFor="name" className="block text-sm font-medium">
Name
</label>
<input
type="text"
id="name"
name="name"
required
className="mt-1 w-full rounded-lg border px-3 py-2"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
type="email"
id="email"
name="email"
required
className="mt-1 w-full rounded-lg border px-3 py-2"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium">
Message
</label>
<textarea
id="message"
name="message"
rows={5}
required
className="mt-1 w-full rounded-lg border px-3 py-2"
/>
</div>
<button
type="submit"
disabled={status === "submitting"}
className="px-6 py-2.5 bg-blue-600 text-white rounded-lg
hover:bg-blue-700 disabled:opacity-50"
>
{status === "submitting" ? "Sending..." : "Send Message"}
</button>
{status === "success" && (
<p className="text-green-600 text-sm">
Message sent! We'll get back to you soon.
</p>
)}
{status === "error" && (
<p className="text-red-600 text-sm">
Something went wrong. Please try again.
</p>
)}
</form>
);
}Adding Client-Side Validation
The form API handles server-side validation, but you should still validate on the client for a better UX:
// Simple validation before submission
function validateForm(data: Record<string, FormDataEntryValue>) {
const errors: string[] = [];
if (!data.name || String(data.name).trim().length < 2) {
errors.push("Name must be at least 2 characters");
}
if (!data.email || !String(data.email).includes("@")) {
errors.push("Please enter a valid email address");
}
if (!data.message || String(data.message).trim().length < 10) {
errors.push("Message must be at least 10 characters");
}
return errors;
}What About Server Actions?
Next.js 14+ introduced Server Actions, which let you run server-side code from form submissions. You could use a Server Action to call the form API instead of doing it client-side. This has a security advantage — your form endpoint ID isn't exposed in the client bundle:
// app/actions.ts
"use server";
export async function submitContactForm(formData: FormData) {
const data = {
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
};
const res = await fetch(
"https://edgesubmit.com/api/submit/YOUR_FORM_ID",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
);
if (!res.ok) throw new Error("Submission failed");
return { success: true };
}When to Use an API vs. Build Your Own
| Scenario | Form API | Custom Backend |
|---|---|---|
| Simple contact form | Best choice | Overkill |
| Lead capture + CRM integration | Good (with webhooks) | Also good |
| Multi-step form with complex logic | Possible | Better fit |
| Forms with file uploads | Check support | Full control |
| High-volume transactional forms | Cost concern | Build your own |
EdgeSubmit — Forms Without the Backend
EdgeSubmit handles form submissions at the edge. Built on Cloudflare Workers for speed, it includes spam filtering, email notifications, and a dashboard to manage submissions. Works with any frontend framework — React, Next.js, Vue, Svelte, or plain HTML.
Try EdgeSubmit Free →Need a custom form solution or full-stack Next.js application? Talk to us — we specialize in building custom web applications.