Limited-time offer The first 2,000 users get 10,000 emails/month free forever 2,000 of 2,000 spots left Claim yours

Guides

Next.js contact form with email (App Router)

Two good ways to send email from Next.js: a Server Action that keeps your key secret, or a client-side form that also works with output: "export".

Last updated October 25, 2026

1Create a project and get a key

Sign up, add a project, connect your SMTP account and copy the key from API & code.

2Option A: Server Action (recommended)

The key stays on the server. Add it to .env.local as FORMSMTP_KEY; leave Allowed websites empty, since requests come from your server.

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

export async function sendContact(_: unknown, formData: FormData) {
  const res = await fetch("https://app.formsmtp.com/api/v1/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FORMSMTP_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      to: "you@yourdomain.com",
      replyTo: formData.get("email"),
      subject: `New message from ${formData.get("name")}`,
      text: String(formData.get("message")),
    }),
  });
  const data = await res.json();
  return data.success ? { ok: true } : { ok: false, error: data.error };
}
tsx
// app/contact/page.tsx
"use client";
import { useActionState } from "react";
import { sendContact } from "./actions";

export default function ContactPage() {
  const [state, action, pending] = useActionState(sendContact, null);
  if (state?.ok) return <p>Thanks! We'll get back to you soon.</p>;
  return (
    <form action={action}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <button disabled={pending}>{pending ? "Sending…" : "Send"}</button>
      {state?.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

3Option B: Client-side (works with static export)

No server needed, so it works with output: "export". Lock the project to your site under Allowed websites and Allowed recipients.

tsx
"use client";
import { useState } from "react";

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

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("sending");
    const res = await fetch("https://app.formsmtp.com/api/v1/form/" + process.env.NEXT_PUBLIC_FORMSMTP_KEY, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new URLSearchParams(new FormData(e.currentTarget) as any),
    });
    setStatus((await res.json()).success ? "sent" : "error");
  }

  if (status === "sent") return <p>Thanks! Message sent.</p>;
  return (
    <form onSubmit={onSubmit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button disabled={status === "sending"}>Send</button>
      {status === "error" && <p role="alert">Something went wrong. Please try again.</p>}
    </form>
  );
}

FAQ

Which option should I use?

Use the Server Action whenever your Next.js app runs on a server (Vercel, Node), since the key never reaches the browser. Use the client-side form for static exports.

Why did my message land in spam?

Usually the From address and the SMTP account don’t match, or the domain has no SPF/DKIM records. Send from the address you log in with, and check the Deliverability panel in FormSMTP’s Settings.

More guides

Ship your form in five minutes

Connect the SMTP account you already have, copy one snippet, done. Free, no card.