DEV Community

Cover image for How to Build a Contact Form in Next.js (Without Building a Backend)
Long N.
Long N.

Posted on • Originally published at formtorch.com

How to Build a Contact Form in Next.js (Without Building a Backend)

Every Next.js project eventually needs a contact form.

And the first instinct is usually:

“I’ll just create an API route and send an email.”

That works.

But it’s also a whole project on its own.

You need to:

  • pick an email provider
  • manage API keys
  • deal with spam
  • handle failures
  • store submissions somewhere

All before you've written a single line of your actual product.

There’s a simpler way.


The options, quickly

When you search “Next.js contact form”, you’ll usually see three approaches:

  1. Route Handler + email API (Resend, SendGrid, etc.)

    Full control, but you own everything.

  2. Server Actions

    Cleaner DX, but you still handle email + spam yourself.

  3. Form backend service

    Send data to a hosted endpoint. It handles storage + email.

For most projects, especially landing pages, SaaS sites, and static apps, option 3 is the fastest path.

This guide walks through that approach.

The simplest version: HTML only

If you don’t need loading states or interactivity, you can use a plain HTML form with no JavaScript at all:

<form action="https://formtorch.com/f/YOUR_FORM_ID" method="POST">
  <input name="name" type="text" placeholder="Your name" required />
  <input name="email" type="email" placeholder="Your email" required />
  <textarea name="message" placeholder="Message" required></textarea>
  <button type="submit">Send message</button>
</form>
Enter fullscreen mode Exit fullscreen mode

That’s already a working form.

The browser handles submission, and you get redirected afterward.

Redirect to your own page

<input
  type="hidden"
  name="_redirect"
  value="https://yoursite.com/thank-you"
/>
Enter fullscreen mode Exit fullscreen mode

For many use cases, this is enough.

But React apps usually want something more interactive.

A proper React contact form

Here’s a production-ready component with loading, success, and error states:

"use client";

import { useState } from "react";

type FormState = "idle" | "loading" | "success" | "error";

export function ContactForm() {
  const [state, setState] = useState<FormState>("idle");

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setState("loading");

    try {
      const data = new FormData(e.currentTarget);

      const res = await fetch("https://formtorch.com/f/YOUR_FORM_ID", {
        method: "POST",
        headers: { "X-Requested-With": "XMLHttpRequest" },
        body: data,
      });

      if (!res.ok) throw new Error();
      setState("success");
    } catch {
      setState("error");
    }
  }

  if (state === "success") {
    return (
      <div>
        <h3>Message sent.</h3>
        <p>Thanks for reaching out. Ill get back to you soon.</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" required />

      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />

      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" required />

      {state === "error" && <p>Something went wrong. Try again.</p>}

      <button disabled={state === "loading"}>
        {state === "loading" ? "Sending…" : "Send message"}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why this works well

  • FormData instead of JSON: Automatically captures all fields (including files).
  • X-Requested-With header: Ensures the response is JSON instead of an HTML redirect.
  • Loading + disabled button: Prevents double submissions.

Adding validation

You can layer client-side validation without changing the structure:

function validate(data: FormData) {
  const errors: Record<string, string> = {};

  if (!data.get("name")) errors.name = "Name is required";

  const email = String(data.get("email") ?? "");
  if (!email) errors.email = "Email is required";
  else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
    errors.email = "Invalid email";

  if (!data.get("message")) errors.message = "Message is required";

  return errors;
}
Enter fullscreen mode Exit fullscreen mode

Add noValidate to disable browser UI and control error rendering yourself.

Using react-hook-form

For more complex forms:

import { useForm } from "react-hook-form";

export function ContactForm() {
  const { register, handleSubmit, formState } = useForm();

  async function onSubmit(data: any) {
    const formData = new FormData();
    Object.entries(data).forEach(([k, v]) =>
      formData.append(k, String(v))
    );

    await fetch("https://formtorch.com/f/YOUR_FORM_ID", {
      method: "POST",
      headers: { "X-Requested-With": "XMLHttpRequest" },
      body: formData,
    });
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name", { required: true })} />
      <input type="email" {...register("email", { required: true })} />
      <textarea {...register("message", { required: true })} />

      <button disabled={formState.isSubmitting}>
        {formState.isSubmitting ? "Sending…" : "Send"}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

What about spam?

Every public form gets spam.

Common approaches:

CAPTCHA (bad UX)

honeypots

rate limiting

spam detection

A simple honeypot field:

<input name="_honeypot" style="display:none" />
Enter fullscreen mode Exit fullscreen mode

Bots fill it. Humans don’t.

The takeaway

You don’t need to build a backend just to send a contact form email.

The practical setup:

  • Simple page → HTML form
  • React app → fetch + FormData
  • Complex form → react-hook-form

Everything else is just plumbing.

Optional: skipping the backend entirely

If you don’t want to deal with email APIs, spam filtering, or storing submissions, you can use a hosted form backend.

For example, Formtorch gives you:

  • a ready-to-use endpoint
  • email notifications
  • built-in spam filtering

So your form works without any server code.

If you want to try it:
👉 Formtorch

Either way, once you understand the flow, contact forms stop being a “setup chore” and become a solved problem.

Top comments (0)