Send Next.js form submissions to email without an API route

A client component posts your Next.js form to an endpoint that stores each submission and emails you. Works on Vercel, in static exports and in both routers.

Free for 100 submissions a month, no credit card. Updated

Most Next.js contact form tutorials add a route handler or a server action that calls an email API. You can skip both. A client component can post the form straight to a Nisuform endpoint, which filters spam, stores the submission and emails you. Because nothing runs on your server, the same form works on Vercel, in a static export and on any other host.

The component

app/contact/ContactForm.tsx
'use client'

import { useRef, useState, type FormEvent } from 'react'

const endpoint = process.env.NEXT_PUBLIC_FORM_ENDPOINT ?? 'https://api.nisuform.com/s/YOUR_FORM_KEY'

export function ContactForm() {
  const shownAt = useRef(Date.now())
  const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
  const [error, setError] = useState('')

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const data = new FormData(event.currentTarget)
    data.set('_elapsed', String(Date.now() - shownAt.current))
    setStatus('sending')
    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: { Accept: 'application/json' },
        body: data,
      })
      const result = await response.json()
      setStatus(result.ok ? 'sent' : 'error')
      setError(result.ok ? '' : result.message)
    } catch {
      setStatus('error')
      setError('Something went wrong. Please try again.')
    }
  }

  if (status === 'sent') {
    return <p role="status">Thanks! We got your message.</p>
  }

  return (
    <form onSubmit={onSubmit}>
      <label>
        Email
        <input name="email" type="email" autoComplete="email" required />
      </label>
      <label>
        Message
        <textarea name="message" rows={5} required />
      </label>
      <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: 'none' }} />
      <button type="submit" disabled={status === 'sending'}>
        {status === 'sending' ? 'Sending...' : 'Send'}
      </button>
      {status === 'error' && <p role="alert">{error}</p>}
    </form>
  )
}

The page that renders it stays a server component, and only the form ships JavaScript:

app/contact/page.tsx
import { ContactForm } from './ContactForm'

export const metadata = { title: 'Contact' }

export default function ContactPage() {
  return (
    <main>
      <h1>Contact</h1>
      <ContactForm />
    </main>
  )
}

The Pages Router ignores the 'use client' line, so the same file works there. Create a form to get your endpoint, then put it in the variable below or straight into the file.

When a server action is worth it

Use a server action when your server needs each message, for example to save it in your own database. The action can still forward the form to the endpoint, but Nisuform then sees your server instead of the visitor. Rate limits count your server's IP address, allowed websites have no Origin to check, and the fill-time check is skipped. The Next.js contact form guide shows that version and what it costs. A static export can't run server actions at all.

The endpoint as an environment variable

The form key is public, so a variable isn't about secrecy. It lets preview deployments post to a test form instead of your real one:

.env.local
NEXT_PUBLIC_FORM_ENDPOINT=https://api.nisuform.com/s/YOUR_FORM_KEY

Next.js inlines NEXT_PUBLIC_ values into the browser bundle at build time. Set the same variable in your host's project settings and redeploy, because a new value only shows up after the next build.

Content Security Policy

If your app sends a Content-Security-Policy header, from next.config.ts or middleware, the browser blocks requests the policy doesn't allow. Add the endpoint's origin to two directives:

Content-Security-Policy
connect-src 'self' https://api.nisuform.com;
form-action 'self' https://api.nisuform.com;

connect-src covers fetch. form-action covers forms that post with a plain action attribute, including the fallback before JavaScript loads. A blocked request shows up in the console as a CSP violation, not as a Nisuform error.

Hosting

Vercel has no forms feature, so on Vercel this is the simplest way to get one. The form never touches your Vercel functions, so it adds nothing to their usage. With output: 'export' the component works unchanged, and you can host the out folder on GitHub Pages, Cloudflare Pages or S3.

Once the site is live, add its domain to the form's allowed websites, plus http://localhost:3000 for development.

What you get with Nisuform

Free covers 100 submissions a month across 3 forms. Pro is $10 a month for 5,000 submissions and adds the features marked Pro.

  • Spam filtering on every plan

    A honeypot, a fill-time check, rate limits and duplicate checks run on every submission, with optional Cloudflare Turnstile or Google reCAPTCHA. Spam never counts toward your limit.

  • An email for every submission

    You get an email within seconds of each submission. Reply-to is set to the visitor, so you can answer from your mail app.

  • Every submission kept

    Search, star and export to CSV. Submissions stay until you delete them, so a missed email never loses a message.

  • File uploadsPro

    Up to 5 images or PDFs per submission, stored privately and shared through links that expire.

  • Slack, Discord, Sheets and webhooksPro

    Send each submission to Slack, Discord, Telegram or Google Sheets, or anywhere else with signed webhooks, Zapier or Make.

  • Auto-repliesPro

    Send each visitor a confirmation in your own words, from your own sender name.

Next.js guides

Frequently asked questions

Do I need an API route to send email from a Next.js form?

No. A client component can post the form to a form backend endpoint, which stores the submission and emails you. Add a route handler or server action only if your own server has to do something with each message, like saving it to your database.

Does Vercel have a forms feature?

No. Unlike Netlify, Vercel has no built-in form handling or email sending. A Next.js form on Vercel either calls your own route handler with an email provider, or posts to a form backend.

Does it work with output export?

Yes. A static export can't run route handlers or server actions, but a client component that posts to an endpoint is plain browser code. It works on GitHub Pages, Cloudflare Pages, S3 or any other static host.

Should the endpoint be an environment variable?

It can be, but it isn't a secret. The form key only accepts new submissions, so it is safe in client code. If you use a variable, prefix it with NEXT_PUBLIC_ so Next.js inlines it into the browser bundle.

Why does my form fail with a Content Security Policy error?

Your CSP blocks requests to other origins. Add https://api.nisuform.com to connect-src for fetch, and to form-action if a form posts with a plain action attribute.

Start collecting form submissions

Set up your forms with built-in spam protection in minutes for free.

Jane Smith

jane@smith.design

Live

12.4k

Submissions

24

Forms

0

Spam