splitforms.com
All articles/ COMPARISONS10 MIN READPublished May 1, 2026

Next.js Server Actions vs form backends: when to use which

Next.js 15+ Server Actions handle form submissions natively. Here's when they're enough — and when a dedicated form backend still wins.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

What Server Actions are

Server Actions are functions in your Next.js code that are guaranteed to run on the server, but can be referenced directly from a Client Component as if they were local. You mark a function with "use server", pass it as the action prop on a <form>, and Next.js handles serialization, the network round-trip, and the response.

They became stable in Next.js 14, and in Next.js 15 and 16 they're the recommended pattern for form mutations. They work without JavaScript (the form just POSTs to a Next-managed RPC endpoint), they integrate with React 19's useActionState and useFormStatus for pending states, and they let you keep server logic next to the component that uses it.

Server Actions are not a form backend. They're the wiring; what you do inside them — store, email, notify, integrate — is up to you.

What a form backend is

A form backend is a hosted endpoint that receives form POSTs and turns them into something useful: an email in your inbox, a row in a dashboard, a webhook into Slack, a JSON event into your data warehouse. The whole point is to skip the server-side glue entirely. Your form's action attribute points at https://splitforms.com/api/submit, and the rest is configuration in a dashboard rather than code in your repo.

splitforms (and Formspree, Web3Forms, Getform, Basin, Formspark) all do this same job. The differences are in pricing, feature depth, and how seriously the team takes deliverability and spam.

Server Action example

A "contact us" form using a Server Action and Resend for email:

// app/contact/page.tsx
import { contactAction } from "./actions";
import { ContactSubmit } from "./submit";

export default function Page() {
  return (
    <form action={contactAction}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <ContactSubmit />
    </form>
  );
}
// app/contact/actions.ts
"use server";

import { Resend } from "resend";
import { redirect } from "next/navigation";

const resend = new Resend(process.env.RESEND_API_KEY!);

export async function contactAction(formData: FormData) {
  const email = String(formData.get("email") ?? "");
  const message = String(formData.get("message") ?? "");

  // ❌ no spam check
  // ❌ no rate limit
  // ❌ no honeypot
  // ❌ no audit log
  // ❌ no retry on Resend failure

  await resend.emails.send({
    from: "forms@yourdomain.com",
    to: "you@yourdomain.com",
    subject: "Contact form",
    text: `From ${email}\n\n${message}`,
  });

  redirect("/contact/thanks");
}
// app/contact/submit.tsx
"use client";
import { useFormStatus } from "react-dom";

export function ContactSubmit() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "Sending..." : "Send"}</button>;
}

30 lines, three files, one third-party API key. It works. The TODOs in the comment block are the work of a productionized form backend.

Form backend example

The same form pointed at splitforms:

// app/contact/page.tsx
export default function Page() {
  return (
    <form action="https://splitforms.com/api/submit" method="POST">
      <input type="hidden" name="access_key" value={process.env.NEXT_PUBLIC_SPLITFORMS_KEY!} />
      <input type="hidden" name="redirect" value="https://yoursite.com/contact/thanks" />

      <input name="email" type="email" required />
      <textarea name="message" required />

      {/* Honeypot — splitforms drops bots automatically */}
      <input type="text" name="botcheck" style={{ display: "none" }} />

      <button type="submit">Send</button>
    </form>
  );
}

No "use server". No Resend client. No rate-limit middleware. No honeypot validation logic. The form is a plain HTML form that works without JavaScript and without the Next.js framework being involved at all. Spam filtering, deliverability, retries, dashboard, webhooks — all configured in the splitforms dashboard, none of it shipped in your Next.js bundle.

Side-by-side comparison

CapabilityServer ActionsForm backend (splitforms)
Time to first working form30–90 min5 min
Works without JSYesYes
App-internal mutationsNativeVia webhook
Email to inboxYou wire Resend/Postmark/SESBuilt-in, deliverability tuned
Submissions dashboardYou build itBuilt-in
Spam filteringYou build itLayered, on by default
Multi-recipient routingYou wire itDashboard config
File uploads to object storageYou wire S3/R2Built-in (Pro+)
Webhook fan-out + retriesYou build itBuilt-in, 24h retry
Audit log / DPA / retentionYou implementBuilt-in artifacts
Vendor lock-inNoneLow — change form action attribute
Cost at 500/mo~$2/mo (Resend)$0 (Free tier)
Cost at 5k/mo~$15/mo + your time$10/mo (Pro)

When Server Actions win

  • App-internal mutations. The form is "create a comment", "update settings", "invite a teammate" — the data lives in your DB and never needs to leave the app.
  • Authenticated forms. You already have the user's session; you want server-side authorization checks before mutating.
  • Optimistic UI is important. React 19's useOptimistic + Server Actions makes optimistic updates trivial.
  • You're building a SaaS dashboard, not a contact form. Server Actions are the right primitive for in-app forms.
  • You enjoy maintaining your own deliverability. Some teams do. Postmark + DKIM + bounce handling is a fine adventure.
  • Strict zero-third-party policy. Some procurement teams refuse any new vendor. Server Actions + your own SMTP is the answer.

When form backends win

  • Marketing site contact forms. Static or near-static content, no in-app state to mutate.
  • Lead capture across multiple sites. One splitforms account routes to one inbox; no Server Action sprawl.
  • You don't want to manage email infrastructure. Inbox rate matters; outsourcing it is rational.
  • You need a dashboard for non-engineers. Marketing wants to read submissions without bothering you.
  • Multi-recipient routing. Sales submissions to one address, support to another, webhooks to a third — config not code.
  • File uploads. Resumes, screenshots, attachments. splitforms handles object-storage and inline-attaches them to the email.
  • Compliance artifacts. You need a DPA, you need EU residency, you need retention policies. SaaS providers ship these as features.

The hidden cost of "just use Resend"

The most common Next.js contact-form pattern in 2026 is "Server Action calls Resend". It works, but the line of code that hides the iceberg is the one that says resend.emails.send(...). The work that endpoint does for you — and the work it does notdo for you — is what determines whether your form is "done" or "pending production for six more weeks."

Things Resend does: SMTP delivery, DKIM signing, IP reputation management, basic bounce tracking. Things Resend does not do: spam filtering, rate limiting per submitter, multi-recipient routing with conditional logic, file uploads, dashboard for non-engineers, audit log for compliance, retry on transient failures with idempotency keys, webhook fan-out, GDPR DPA covering the form-submission processing chain.

For a personal site or an internal tool, none of that matters. For a form on a marketing site that captures sales leads, every one of those gaps is a real cost — measured in spam clogging your inbox, leads dropped because Resend hiccupped, sales reps who can't see submission history without bothering an engineer, and a procurement reviewer asking for a DPA you don't have. splitforms exists because the "Server Action + Resend" pattern has a ceiling, and the ceiling shows up faster than people expect.

Security: what you have to remember

Server Actions look like local function calls but they're HTTP endpoints that anyone on the internet can hit. The Next.js team has documented the attack surface (see the security model in node_modules/next/dist/docs/01-app/02-guides/data-security.md) but the responsibility to apply it is yours. At minimum:

  • Validate every input. Use zod, valibot, or yup — never trust FormData.get() directly. The action runs on the server with whatever bytes the client sent.
  • Authorize before mutating. Re-check session and permissions inside the action; don't rely on the fact that the page that rendered the form was server-rendered for a logged-in user.
  • Rate limit per IP and per session. Server Actions inherit no rate limiting unless you add it. @upstash/ratelimit is the typical answer.
  • Add CSRF protection if your action mutates. Next.js sets a same-origin check by default; if you've relaxed it via experimental.allowedRevalidateHeaderKeys or similar, double-check.
  • Don't leak server-only data through the response. Anything you return ships to the client.

splitforms handles the equivalent — rate limiting, IP reputation, payload validation against your form schema, abuse detection — server-side, behind a single endpoint. You don't maintain it; we do.

Using both together

The composable pattern: a Server Action handles the in-app side effects (insert into DB, generate a record, kick off a background job), and inside the Server Action you POST to splitforms for the human-facing notifications. Or invert it: the form goes to splitforms, splitforms webhooks back into a Next.js Route Handler that does the app-internal work.

// app/api/lead/route.ts — receives splitforms webhook
import { NextRequest } from "next/server";
import crypto from "node:crypto";

export async function POST(req: NextRequest) {
  const raw = await req.text();
  const sig = req.headers.get("x-splitforms-signature") ?? "";
  // ... HMAC verification ...

  const { fields } = JSON.parse(raw);
  await db.lead.create({ data: { email: fields.email, message: fields.message } });
  await notifySalesRep(fields.email);

  return new Response("ok");
}

The form is one line of HTML, the deliverability is splitforms' problem, and your app code only runs on confirmed-good submissions because spam filtering already happened upstream.

Tech support and troubleshooting

Five things that bite Next.js teams running their own form Server Actions:

  • Server Action emails land in spam — sending domain isn't verified in Resend/Postmark. Add the SPF and DKIM records before going live.
  • Form submission silently dropped — the action threw an error after the redirect; useActionState swallows it. Wrap the body in try/catch and surface a real error state.
  • Spam wave melted your function quota — Server Actions don't ship spam filtering. Either add hCaptcha + a honeypot, or front the form with splitforms so spam is dropped at the edge.
  • File uploads fail at 4MB — Vercel's body size cap. Either presign to S3/R2 from the client, or POST to splitforms which streams to object storage.
  • Reply-To not respected — you set From to the visitor's email; Gmail rejects via DMARC. Always send From a domain you own and put the visitor address in Reply-To.

The full webhook spec, retry policy, and signature reference are in the splitforms docs and the API reference; account questions live in the splitforms FAQ.

FAQ

Are Next.js Server Actions production-ready for forms?

Yes. Server Actions stabilized in Next.js 14 and have been the recommended Next pattern for form mutations since Next.js 15. They handle progressive enhancement, work without JavaScript, and integrate cleanly with React 19's useActionState.

When should I use a form backend instead of a Server Action?

When you need any of: deliverable email out of the box, a submissions dashboard, multi-recipient routing, file uploads to object storage, webhook fan-out, spam classification you don't want to maintain, or compliance artifacts (DPA, audit log, retention controls). Server Actions get you to a database insert; a form backend gets you to an inbox.

Can I use both — Server Actions and a form backend?

Yes, and many production apps do. The Server Action handles app-internal mutations (creating a record, charging Stripe), then forwards a 'send me an email about this' job to a form backend like splitforms. Best separation of concerns: business logic stays in your app, deliverability stays out of your codebase.

Do Server Actions work without JavaScript?

Yes. When you bind a Server Action to a form's `action` prop, Next.js renders it as a regular <form> POST that works without JS. JavaScript progressively enhances with useTransition for pending states.

Does using a form backend hurt my Next.js bundle size?

No — splitforms is just a POST endpoint. You don't import a client library, you don't ship a script, you don't add a runtime dependency. The form's action attribute points at our URL and the browser does the rest.

Next steps

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

1,000 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your inbox.

Generate access key →Read the docs
v0.1 · founders pricing locked in · early access open