Skip to main content
New:Build with AI agents

Next.js contact forms
without mail plumbing.

Use App Router Server Actions or a client component while Form Plume handles delivery, dashboard storage, spam filtering, uploads, integrations, and signed webhooks.

Free foreverNo credit cardNext.js endpoint ready in under a minute
app/contact/actions.ts
// app/contact/actions.ts
"use server";

const endpoint = "PASTE_YOUR_FORM_PLUME_ENDPOINT_HERE";

export async function submitContact(formData: FormData) {
  const response = await fetch(endpoint, {
    method: "POST",
    body: formData,
  });

  if (!response.ok) {
    throw new Error("Submission failed");
  }
}

The full guide

A hosted backend for your Next.js contact form.

Choose the Server Action or client-component pattern that fits your route, then let Form Plume own the email and submission pipeline.

  1. 1
    Create your Form Plume accountStart free and give your Next.js form a hosted endpoint for email, submissions, spam filtering, uploads, integrations, and webhooks.Start free
  2. 2
    Create the form and copy the endpointCopy the endpoint URL from Form Plume. It looks like https://api.formplume.com/f/your-slug
  3. 3
    Connect it inside Next.jsChoose a Server Action for progressive enhancement or a client component when the page already needs client-side state. Send one test submission from the real Next.js app or preview environment and confirm it reaches Form Plume.

You can use this AI prompt to hand the platform-specific wiring to an assistant without accidentally creating a backend.

Use an App Router Server Action

Server Actions let a Next.js form submit through your route without writing an API handler. The action forwards the FormData to Form Plume, then redirects or returns an error state.

// app/contact/actions.ts
"use server";
 
import { redirect } from "next/navigation";
 
const FORM_PLUME_ENDPOINT = "PASTE_YOUR_FORM_PLUME_ENDPOINT_HERE";
 
export async function submitContact(formData: FormData) {
  const response = await fetch(FORM_PLUME_ENDPOINT, {
    method: "POST",
    body: formData,
  });
 
  if (!response.ok) {
    throw new Error("Submission failed");
  }
 
  redirect("/thanks");
}
// app/contact/page.tsx
import { submitContact } from "./actions";
 
export default function ContactPage() {
  return (
    <form action={submitContact}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
 
      <label htmlFor="email">Email</label>
      <input id="email" type="email" name="email" autoComplete="email" required />
 
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={5} required />
 
      <button type="submit">Send message</button>
    </form>
  );
}

The user sees a normal form. Form Plume receives the submission, sends email, stores the message, filters spam, handles files, and triggers integrations without a custom email service.

Add a pending button

Use a tiny client component for pending state instead of converting the whole page.

// app/contact/submit-button.tsx
"use client";
 
import { useFormStatus } from "react-dom";
 
export function SubmitButton() {
  const { pending } = useFormStatus();
 
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Sending..." : "Send message"}
    </button>
  );
}

Client component option

If the contact form lives inside an already interactive component, post directly to Form Plume from the browser.

"use client";
 
import { useState, type FormEvent } from "react";
 
const FORM_PLUME_ENDPOINT = "PASTE_YOUR_FORM_PLUME_ENDPOINT_HERE";
 
export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
 
  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus("sending");
 
    const response = await fetch(FORM_PLUME_ENDPOINT, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(event.currentTarget),
    });
 
    setStatus(response.ok ? "sent" : "error");
  }
 
  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button disabled={status === "sending"}>Send message</button>
      {status === "sent" && <p role="status">Thanks, we got your message.</p>}
      {status === "error" && <p role="alert">Try again in a moment.</p>}
    </form>
  );
}

Next.js-specific warnings

Do not place secret mail credentials in NEXT_PUBLIC_ variables. Public variables are bundled into the browser.

Do not add an API route only to call an email provider. If you want the visitor to submit to your Next.js server first, a Server Action is usually enough, and Form Plume remains the hosted form backend.

Use FormData for attachments. JSON cannot carry files without extra work.

Server Actions are a good default for App Router pages because they keep the form progressive and avoid client JavaScript for the basic submit path.

Primary sources

FAQ

Next.js form questions
before launch.

For App Router pages, a Server Action is a strong default because the form stays progressive and the action can forward FormData to Form Plume without creating an API route.

One line. Zero backend.

The form backend you don’t have to build.

Free foreverNo credit cardSet up in under a minute