Form Handling in Next.js: Server Actions vs API Routes vs Form Backend
Three ways to handle forms in Next.js
Next.js gives you multiple ways to handle form submissions. Each has trade-offs. Let's compare them honestly.
Option 1: Server Actions
Next.js 14+ introduced server actions — functions that run on the server and can be called directly from client components.
"use server";
export async function submitContactForm(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const message = formData.get("message") as string;
// Validate
if (!name || !email || !message) {
return { error: "All fields are required" };
}
// Store in database
await db.submission.create({
data: { name, email, message },
});
// Send notification email
await sendEmail({
to: "you@example.com",
subject: `New contact from ${name}`,
body: message,
});
return { success: true };
}Pros:
- No API route needed — less boilerplate
- Type-safe end-to-end with TypeScript
- Progressive enhancement — works without JavaScript
- Built into Next.js — no extra dependencies
Cons:
- You still need to write the email sending logic
- You still need to set up spam protection
- You still need a database for storing submissions
- No dashboard for viewing submissions
- Tightly coupled to your Next.js app
Best for: Forms where submission data maps directly to your database models and you already have email infrastructure set up.
Option 2: API Routes
The traditional approach — create a POST endpoint in app/api/:
// app/api/contact/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body = await request.json();
const { name, email, message } = body;
if (!name || !email || !message) {
return NextResponse.json(
{ error: "All fields required" },
{ status: 400 }
);
}
// Store, send email, etc.
await db.submission.create({ data: { name, email, message } });
await sendEmail({ /* ... */ });
return NextResponse.json({ success: true });
}Pros:
- Standard REST pattern — works with any frontend
- Easy to test independently
- Can be consumed by mobile apps, other services
- Full control over request/response
Cons:
- Same as server actions: you own all the infrastructure
- More boilerplate than server actions
- Need to handle CORS if used cross-origin
Best for: Forms that need to be consumed by multiple frontends, or when you want a REST API for other integrations.
Option 3: Form backend service (InputHaven)
Point your form directly at a third-party endpoint:
"use client";
import { useState } from "react";
export function ContactForm() {
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("loading");
const res = await fetch("https://inputhaven.com/api/v1/submit", {
method: "POST",
body: new FormData(e.currentTarget),
headers: { Accept: "application/json" },
});
setStatus(res.ok ? "success" : "error");
}
if (status === "success") return <p>Thank you! We'll be in touch.</p>;
return (
<form onSubmit={handleSubmit}>
<input type="hidden" name="_form_id" value="your-form-id" />
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button disabled={status === "loading"}>
{status === "loading" ? "Sending..." : "Send"}
</button>
</form>
);
}Pros:
- Zero backend code — no server action, no API route
- Spam protection built in (honeypot, keywords, AI, rate limiting)
- Email notifications configured in dashboard
- Submissions dashboard with search and export
- File uploads, webhooks, auto-responses included
- Works if you move away from Next.js
Cons:
- Third-party dependency
- Monthly cost at scale (though free tier covers most use cases)
- Less control over server-side processing logic
Best for: Contact forms, feedback widgets, lead capture, newsletter signups, and any form where you don't need custom server-side business logic.
Decision framework
| Question | Server Action | API Route | Form Backend |
|---|---|---|---|
| Need custom business logic? | Yes | Yes | No |
| Need email notifications? | Build it | Build it | Included |
| Need spam protection? | Build it | Build it | Included |
| Need a submissions dashboard? | Build it | Build it | Included |
| Need file uploads? | Build it | Build it | Included |
| Need webhooks? | Build it | Build it | Included |
| Works without Next.js? | No | Maybe | Yes |
| Time to implement? | Hours–days | Hours–days | Minutes |
The hybrid approach
You can combine approaches. Use server actions for forms tightly integrated with your app (like profile settings), and use InputHaven for standalone forms (like contact pages and feedback widgets).
This gives you the best of both worlds: custom logic where you need it, and zero maintenance where you don't.
Getting started with InputHaven + Next.js
- Create a free InputHaven account
- Create a form in the dashboard
- Copy the form ID
- Use the code example above
The entire integration is one fetch call with a form ID. No API keys, no backend code, no database setup.