[ ENGINEERING_GUIDE ][ NEXTJS ][ SERVER_ACTIONS ][ FORMS ][ API_ROUTES ]

Next.js 15 Server Actions vs Route Handlers — contact forms in 2026

June 10, 202610 min read
Author: DevStudio.itWeb & AI Studio

When to use Server Actions vs staying on an API Route. Validation, revalidatePath, CSRF, and the pattern from a production contact form on Next.js 15.5.18.

READ_TIME: 10 MIN_COMPLEXITY: MED_
STAMP: VERIFIED_BY_DS_

TL;DR

Server Actions in Next.js 15 are the default path for form mutations — no separate REST endpoint, built-in protection against simple CSRF attacks, and natural integration with React 19. Route Handlers (/api/...) still win when you need webhooks from external systems, per-IP rate limiting, server-side reCAPTCHA verification, CRM integrations over REST, or when the form lives in a large client component with post-success analytics (gtag). On the production DevStudio.it site, the form in page.tsx sends POST to /api/submissions — a deliberate choice, not “legacy pattern”. Below: comparison, Zod validation, revalidatePath, security, and a step-by-step migration plan.

Who this is for

  • Developers building lead forms on Next.js 15 (App Router)
  • Teams considering migration from API Route to Server Action (or the reverse)
  • Product owners who want to understand why “simpler code” is not always the better choice
  • Companies with GDPR, reCAPTCHA, i18n (/pl, /en, /de), and conversion analytics after submit

Keywords (SEO)

nextjs server actions contact form, server actions vs api route nextjs, revalidatePath form, nextjs 15 form validation, csrf server actions nextjs

Two ways to handle forms in the App Router

Next.js 15 offers two main server-side mutation paths:

Aspect Server Action Route Handler (/api/...)
Invocation action={submitContact} or formAction fetch('/api/submissions', { method: 'POST' })
Data format FormData (native) or serialized object JSON, multipart, any
CSRF Built-in Next.js tokens You must secure it yourself (or rely on SameSite cookies)
External webhooks No — invocation from your site only Yes — standard REST endpoint
Cache / revalidation revalidatePath, revalidateTag Manual after mutation or separate action
Testing Harder outside RSC context Postman, curl, CI integrations

A Server Action is a function marked 'use server' that React calls directly from a form. Next.js serializes the result, handles POST with a hidden action field, and — unlike classic APIs — does not expose a public URL that anyone could spam with curl (though technically a POST to the current page exists).

A Route Handler is a route.ts file under app/api/, familiar from the Pages Router and the REST ecosystem. It remains the default when submit logic goes beyond “save to DB and send email”.

Production pattern: form in page.tsx + /api/submissions

In the DevStudio.it project (Next.js 15.5.18), the contact form lives in a large client component src/app/[locale]/page.tsx. After client-side email validation and reCAPTCHA v3 execution, it sends:

const response = await fetch('/api/submissions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name, email, phone, projectType, budget, description,
    locale,
    recaptchaToken,
  }),
});

After response.ok, the page fires GA4 (generate_lead) and Google Ads (conversion_event_submit_lead_form) events, then redirects to the thank-you page with event_timeout: 2000 — so tags finish before navigation.

The Route Handler in src/app/api/submissions/route.ts does much more than a simple insert:

  • Rate limiting per IP (checkEmailRateLimit)
  • reCAPTCHA verification with Google (siteverify, score threshold 0.5)
  • Prisma persistence (submission.create)
  • Transactional emails (admin + client, templates per locale)
  • IP and User-Agent logging

This is not “we stayed on the old API by accident”. It is architecture where submit is a mini integration pipeline, and client success must stay in sync with marketing analytics — which with Server Actions requires either moving the whole flow to RSC or a hybrid approach.

Server Actions — what they look like in Next.js 15

Minimal Server Action example for a contact form:

// app/actions/contact.ts
'use server';

import { z } from 'zod';
import { revalidatePath } from 'next/cache';

const contactSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  description: z.string().min(10).max(5000),
  locale: z.enum(['pl', 'en', 'de']),
});

export type ContactFormState = {
  ok: boolean;
  message?: string;
  fieldErrors?: Record<string, string[]>;
};

export async function submitContact(
  _prev: ContactFormState,
  formData: FormData
): Promise<ContactFormState> {
  const parsed = contactSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    description: formData.get('description'),
    locale: formData.get('locale'),
  });

  if (!parsed.success) {
    return {
      ok: false,
      fieldErrors: parsed.error.flatten().fieldErrors,
    };
  }

  // ... DB save, email, reCAPTCHA ...

  revalidatePath(`/${parsed.data.locale}`);
  return { ok: true, message: 'Sent successfully' };
}

Form with React 19 useActionState (formerly useFormState):

'use client';

import { useActionState } from 'react';
import { submitContact } from '@/app/actions/contact';

export function ContactForm() {
  const [state, formAction, pending] = useActionState(submitContact, { ok: false });

  return (
    <form action={formAction}>
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="description" required />
      <input type="hidden" name="locale" value="en" />
      <button type="submit" disabled={pending}>
        {pending ? 'Sending…' : 'Send'}
      </button>
      {state.fieldErrors?.email && <p>{state.fieldErrors.email[0]}</p>}
      {state.ok && <p>{state.message}</p>}
    </form>
  );
}

Benefits:

  • Progressive enhancement — form works without JS (full page submit)
  • No manual fetch and JSON parsing on the client
  • Validation and mutation in one server place
  • pending from the hook — built-in loading state

Validation: client vs server

The 2026 rule is unchanged: client validation is UX; server validation is security.

In the current project, the client checks email format before send (validateEmail), and the Route Handler verifies required fields and rejects requests without a reCAPTCHA token. That split is correct.

With Server Actions, Zod (or Valibot) at the entry point is standard:

const parsed = contactSchema.safeParse(Object.fromEntries(formData));

Additional patterns:

  • Normalizationemail.trim().toLowerCase() before save
  • Honeypot — hidden website field; if filled, silent success without save
  • Length limits — protect against 10 MB payloads in description
  • Consistent error messages per locale — map Zod codes to translations from messages/en.json

Never trust HTML required as the only barrier — bots and curl bypass it.

revalidatePath and UI refresh

After a successful save, Server Actions often call:

import { revalidatePath } from 'next/cache';

revalidatePath('/en/admin');           // specific path
revalidatePath('/en', 'layout');       // entire layout segment
revalidateTag('submissions');          // if list uses fetch with tag

revalidatePath invalidates RSC cache for that route — admin sees the new lead without hard refresh. In a Route Handler you must either:

  • call the same function from a shared module (import { revalidatePath } from 'next/cache' works in route.ts),
  • or rely on SWR / React Query on the client with mutate() after successful fetch.

For a public contact form, revalidatePath is rarely needed — users do not see the submission list. It becomes critical for admin panel forms or blog comments.

Security: CSRF and Server Actions

Next.js Server Actions send POST with an action header and token that the framework verifies. That limits classic CSRF from foreign domains — an attacker cannot generate a valid token without knowing the build secret / origin.

Route Handlers do not have this protection by default. Public POST /api/submissions can be spammed:

  • Rate limiting (as in the project) — first line of defense
  • reCAPTCHA v3 — second line
  • Origin / Referer verification — extra layer
  • API key in header — when the endpoint serves only your app (not a public form)

In Server Actions, reCAPTCHA is still required — generate the token on the client (grecaptcha.execute), pass it in FormData, verify in the action with the same code as in the Route Handler.

Note: Server Actions do not replace botnet protection — they only simplify the CSRF model for first-party forms.

When to stay on a Route Handler (API route)

Stay on /api/submissions (or similar) when:

1. External integrations and webhooks

Stripe, Calendly, Typeform send POST to a fixed URL. Route Handlers are the natural place. Server Actions have no public address to configure in the Stripe dashboard.

2. The client is already client-heavy

The form in page.tsx has hundreds of lines of UI (Framer Motion animations, sticky header, portfolio). Moving submit to Server Actions requires either extracting the form into a component with useActionState, or keeping fetch — both are fine.

3. Post-success analytics in the browser

gtag('event', 'generate_lead') must run in the client after HTTP 200. A Server Action can return { ok: true } and the client still calls gtag — hybrid works. But if you redirect() inside the action to /thank-you, you lose the moment for Ads conversion callbacks. Hence the current pattern: fetch → gtag with event_callback → redirect.

4. Testing and monitoring

REST endpoints are easy to test in Postman, load tests (k6), and reverse proxy logs (POST /api/submissions 429). Server Actions log as POST to a page — less readable in nginx access logs.

5. Multiple consumers of the same API

Mobile app, chatbot (/api/chatbot/submit), WordPress widget — one Route Handler, many clients. In the project the chatbot has a separate endpoint, but the logic is analogous.

When to choose Server Actions

  • New, simple form in RSC or a small client component
  • No gtag-before-redirect requirement — thank-you page is enough
  • You want progressive enhancement without writing a separate API
  • Admin panel — content edits, submission status, comments with revalidatePath
  • Smaller surface area — one action file instead of route.ts + response types

Migration plan: API Route to Server Action (optional)

If you migrate a DevStudio-style form:

  1. Extract logic from route.ts into lib/submissions/create-submission.ts (pure async function).
  2. Create app/actions/submit-contact.ts with 'use server' — calls shared lib + revalidatePath('/en/admin').
  3. Keep POST /api/submissions as a thin wrapper calling the same lib (backward compatibility, tests).
  4. Extract the form from page.tsx into ContactFormSection.tsx with useActionState.
  5. Keep the gtag block after state.ok in useEffect — analytics stays on the client.
  6. Test reCAPTCHA, rate limit, and emails on staging.

Migration does not have to be big bang. Shared lib + two entrypoints is a mature intermediate stage.

Progressive enhancement vs SPA submit

Server Actions with native <form action={...}> work without JavaScript. The current form with onSubmit, reCAPTCHA, and fetch requires JS — users without scripts cannot send a lead. For a B2B software house site that is acceptable (99%+ traffic has JS), but document it consciously in the spec.

If GDPR requires operation without tracking cookies, Server Action + redirect to /thank-you delivers the lead to CRM without gtag — consistent with “Necessary only” in the cookie banner.

FAQ

Do Server Actions replace Route Handlers in Next.js 15?

Not entirely. Actions are optimal for mutations from your React app. Route Handlers remain standard for webhooks, public REST, multipart uploads, and third-party integrations. One project often has both.

Is a Server Action safer than an API route?

For CSRF — yes, by default. For spam and bots — no; you still need rate limits, CAPTCHA, and server validation. A public endpoint is a public endpoint — Actions can also be mass-invoked from automation if someone reverse-engineers the token (harder than curl on /api/..., but not impossible).

How do I pass a reCAPTCHA token in a Server Action?

Generate the token in 'use client' before submit (grecaptcha.execute), add as <input type="hidden" name="recaptchaToken" value={token} /> or append to FormData on programmatic submit. In the server action, call the same fetch to google.com/recaptcha/api/siteverify as in the Route Handler.

Can I use redirect() in a Server Action after success?

Yesimport { redirect } from 'next/navigation'. Remember: redirect in an action interrupts render and does not return JSON to the client. If you need gtag first, redirect on the client (current pattern) or put conversion tags on the thank-you page layout.

Is revalidatePath needed after a contact form?

Usually not on the public site — users do not see a cached list. Yes in admin panel, blog with comments, or when you render “your recent submissions” after submit.

JSON or FormData in Server Actions?

FormData — native <form> format. For complex fields you can serialize JSON into one hidden field. Avoid sending entire React state — only form data.

Summary

Next.js 15.5.18 does not force Server Actions on forms — it offers a better alternative for simple mutations with progressive enhancement and built-in CSRF. The production form on DevStudio.it with fetch('/api/submissions'), reCAPTCHA, rate limiting, emails, and gtag after success is a justified Route Handler, not technical debt. Choose Server Actions when submit is simple and lives close to RSC; stay on API when submit is an integration pipeline, webhooks, or tight sync with Google Ads. In both cases: Zod on the server, never HTML required alone, and a shared lib if you plan migration.

Want a form implemented on your site?

  • Contact us — we will design a form for your campaign and stack
  • Websites — Next.js 15, SEO, conversions, and security
  • See our work — production deployments with analytics

About the author

We build fast websites, web/mobile apps, AI chatbots and hosting setups — with a focus on SEO and conversion.

Recommended links

From theory to production — Branchly, our hosting stack, care plans and shipped work.

LIKE HOW WE THINK? LET'S BUILD SOMETHING TOGETHER.

[ START_PROJECT_CONFIGURATION ]