NEW AI agents now first-class: authorize · audit · revoke in one click — your agents submit cleanly, bots stay blocked. Read agent docs →

Add a working form to a Next.js 14 app in 10 minutes

A real Next.js 14 App Router form: a client component, a Server Action to a hosted backend, success/error states, and spam protection — no API route.

Next.js makes the UI easy and the backend of a form annoying: you either stand up an API route, wire an email provider, and handle spam yourself, or you reach for a hosted form backend and skip all of it. This is the second path, done properly with the App Router — Server Action included.

You’ll have a working contact form, emailing you on every submission, with no server code of your own to maintain.

Step 1 — Create the form endpoint

Sign up, create a form, set your fields (name, email, message), and copy the endpoint:

https://login.ollastack.com/api/submit/<your-slug>

That URL accepts JSON and form-encoded bodies and returns 201 with { success: true, id }. That’s your entire backend.

Step 2 — The client component

"use client";
import { useState } from "react";
import { submitContact } from "./actions";

export function ContactForm() {
  const [state, setState] = useState<"idle" | "sending" | "sent" | "error">("idle");

  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        setState("sending");
        const data = new FormData(e.currentTarget);
        const ok = await submitContact(data);
        setState(ok ? "sent" : "error");
      }}
    >
      {state === "sent" ? (
        <p>Thanks — we'll be in touch.</p>
      ) : (
        <>
          <input name="name" placeholder="Name" required />
          <input name="email" type="email" placeholder="you@example.com" required />
          <textarea name="message" placeholder="Message" required />
          {/* Honeypot: real users never see it; bots fill it. */}
          <input name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: "none" }} />
          <button disabled={state === "sending"}>
            {state === "sending" ? "Sending…" : "Send"}
          </button>
          {state === "error" && <p>Something went wrong — try again.</p>}
        </>
      )}
    </form>
  );
}

Step 3 — The Server Action

Keeping the POST on the server means the endpoint isn’t called straight from the browser, and you can add server-only logic later (logging, redirects) without touching the component.

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

export async function submitContact(form: FormData): Promise<boolean> {
  const res = await fetch(
    "https://login.ollastack.com/api/submit/your-slug",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name: form.get("name"),
        email: form.get("email"),
        message: form.get("message"),
        _gotcha: form.get("_gotcha"), // honeypot passes through
      }),
    }
  );
  return res.ok;
}

That’s the whole integration. No API route, no Nodemailer, no spam library.

Step 4 — Notifications

In the form’s settings, set the email that should receive submissions (verify it first — a form can only notify a verified address, which prevents your form from being pointed at a stranger’s inbox). Want replies to go straight to the person who wrote in? The submission’s email field is automatically used as the notification’s Reply-To, so hitting “Reply” answers the lead.

Step 5 — Spam, without writing any

You already added the honeypot. Beyond that, submissions run through a layered pipeline (IP blocklist, keyword/regex, link limits, Akismet, and an ML classifier) automatically. The important guarantee: a real lead is never silently dropped — anything the ML model alone is unsure about is delivered and labeled [Possible spam], not deleted, and you can recover it in one click.

Optional — render it server-side or skip the UI entirely

Two shortcuts worth knowing:

  • No UI at all: every form gets a hosted page at login.ollastack.com/f/<slug> — link to it and you’re done.
  • Progressive enhancement: because the endpoint also accepts a plain multipart/form-data POST, the same form works with action="https://login.ollastack.com/api/submit/your-slug" method="POST" even before JavaScript loads. The Server Action above just gives you inline success/error states.

Where to go next

  • Forward submissions to Slack/Discord or your own webhook (signed, with retries + replay).
  • Let an AI agent submit the same form with a scoped Bearer token (it discovers the API from /api/openapi.json).
  • Add file uploads (the endpoint handles multipart/form-data).

Create your form — free tier is 100 submissions/month, no credit card. The endpoint is the only thing you have to remember.

Frequently asked questions

How do I add a form to Next.js 14?

Use a client component with a Server Action that POSTs to a hosted form endpoint. You get email, spam filtering and webhooks with no API route of your own.

Do I need an API route?

No — a Server Action or a direct fetch posts to the hosted endpoint; there's no email or spam plumbing to build.

Does it handle spam?

Yes — a layered spam pipeline runs server-side, with an optional honeypot field.

Last updated June 19, 2026. Spotted something out of date? Email hello@ollastack.com.