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
'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:
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:
NEXT_PUBLIC_FORM_ENDPOINT=https://api.nisuform.com/s/YOUR_FORM_KEYNext.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:
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.