Framework // Next.js

Next.js contact form, no API route

Next.js is a full-stack framework, so you could build a Route Handler and wire up an email SDK. For a contact or lead form that’s more surface area than the job needs. The App Router gives you a cleaner option, a Server Action that forwards to an endpoint, or a plain client-side fetch. Both skip the /api route entirely.

The short answer

Next.js can render a contact form, but the App Router gives you no place to email submissions by default. Two options work: a client fetch, or a Server Action that forwards the fields to a MakeTheForm endpoint. Either way the endpoint filters spam, emails you, and stores each submission: no /api route to build.

The minimal integration

This is a Server Action in the App Router: no /api route, no database. It runs on the server and forwards to your endpoint; pair it with useActionState for pending state. A client-side fetch works too if your form is a client component.

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

export async function submitContact(prevState, formData) {
  const response = await fetch('https://mtform.co/f/your-form-key', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(Object.fromEntries(formData)),
  });

  const result = await response.json();

  if (!response.ok) {
    return { ok: false, message: result.error?.message ?? 'Unable to send your message.' };
  }

  return { ok: true, message: 'Thanks. We got your message.' };
}

// app/contact/page.tsx
'use client';
import { useActionState } from 'react';
import { submitContact } from './actions';

export default function ContactPage() {
  const [state, action, pending] = useActionState(submitContact, null);

  return (
    <form action={action}>
      <input name="name" placeholder="Name" required />
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <input name="_mtf_honeypot" tabIndex={-1} autoComplete="off" aria-hidden="true"
             style={{ position: 'absolute', left: '-9999px' }} />

      <button type="submit" disabled={pending}>{pending ? 'Sending…' : 'Send'}</button>
      {state && <p role={state.ok ? 'status' : 'alert'}>{state.message}</p>}
    </form>
  );
}

A Server Action: no API route needed. Returns a serializable result the form renders inline.

Step by step

  1. 01

    Create the endpoint

    Create a MakeTheForm form to get your public endpoint URL and inbox.

  2. 02

    Add a Server Action

    In a "use server" file, write one function that fetches the endpoint with the submitted FormData as JSON and returns a serializable { ok, message }.

  3. 03

    Wire the form and pending state

    Bind the action with useActionState, disable the button while pending, and render the returned message inline.

  4. 04

    Verify and deploy

    Verify your recipient email once, deploy to Vercel or your host, and submit to confirm delivery to your inbox and email.

Common pitfalls

Assuming a Server Action needs a secret key

The form endpoint isn’t a secret. It’s a public address, and it works the same from the browser. A Server Action is a fine place to call it from, but don’t assume you now need an env-var API key. There isn’t one.

Mixing "use server" into a "use client" file

You can’t declare a Server Action inside a client component. Put the action in its own "use server" module and import it, or the build fails. Keep the form component and the action in separate files.

Troubleshooting

Troubleshooting by error code
SymptomWhat is happeningFix
action not firingThe form uses onSubmit instead of the action prop, so the Server Action never runs.Bind it with <form action={action}> from useActionState, or call the action from your handler.
pending never endsThe action throws instead of returning, so useActionState can’t settle.Catch fetch errors and return { ok: false, message } rather than throwing inside the action.
403Allowed-domains is on and your deployed domain isn’t listed.Add your production and preview domains in the form’s spam settings, or leave it public.

Frequently asked questions

Can I build a Next.js contact form without a backend?

Yes. Point the form at a hosted endpoint. Either via a client fetch or a Server Action that forwards to it. The endpoint receives, spam-filters, emails, and stores each submission, so you don’t build or host a backend of your own.

How do I use a Server Action to send a form to email?

Write a "use server" function that fetches an email-capable endpoint with the submitted FormData and returns a result. Bind it with useActionState for pending and error state. The endpoint handles the actual email delivery and spam filtering.

Do I need an /api route for a Next.js form?

No. A Server Action or a direct client-side fetch to a form endpoint both work without a Route Handler. You only need an /api route for custom server processing the endpoint doesn’t already cover.

Ship this form for real

Create an endpoint, paste it in, and watch the first submission land in your inbox, in under three minutes.

Create free endpoint