Ship a contact form on Next.js in four minutes (no API route)
April 4, 2026 · 6 min read
Here's a small confession. Every Next.js project I've started in the last two years eventually sprouted a /api/contact route, and every single one of them eventually gave me grief. Rate limits I forgot to add. Spam I forgot to filter. A Vercel cold start that made the thank-you page feel janky. A Resend quota I forgot I was paying for.
At some point I just stopped writing the route. I post the form directly to a hosted endpoint and let someone else worry about the ingestion. Everything still works. My Sundays are quieter.
Here is the whole setup for App Router, in the time it takes to boil a kettle.
What you need
- A Next.js project (App Router — same idea works on Pages Router)
- A form URL from any hosted backend. I'll use FormTo:
https://api.formto.dev/f/your-form-slug
That's the whole dependency list. No resend, no zod (unless you want it), no server action, no middleware.
The form
Create app/contact/page.tsx:
const endpoint = 'https://api.formto.dev/f/your-form-slug'
export const metadata = {
title: 'Contact us',
}
export default function ContactPage() {
return (
<main className="mx-auto max-w-lg px-6 py-16">
<h1 className="text-4xl font-bold mb-6">Say hi</h1>
<p className="text-gray-600 mb-8">
Fastest way to reach us. We reply within a day.
</p>
<form method="POST" action={endpoint} className="space-y-4">
<label className="block">
<span className="text-sm font-medium">Name</span>
<input
name="name"
type="text"
required
className="mt-1 w-full border rounded-md px-3 py-2"
/>
</label>
<label className="block">
<span className="text-sm font-medium">Email</span>
<input
name="email"
type="email"
required
className="mt-1 w-full border rounded-md px-3 py-2"
/>
</label>
<label className="block">
<span className="text-sm font-medium">Message</span>
<textarea
name="message"
rows={5}
required
className="mt-1 w-full border rounded-md px-3 py-2"
/>
</label>
{/* honeypot */}
<input
type="text"
name="website"
tabIndex={-1}
autoComplete="off"
className="hidden"
aria-hidden="true"
/>
<input type="hidden" name="_redirect" value="https://yoursite.com/thanks" />
<button
type="submit"
className="px-5 py-2 bg-black text-white rounded-md hover:bg-gray-800"
>
Send
</button>
</form>
</main>
)
}
That's the entire page. It's a server component by default, which is what you want. No 'use client'. No state. No useEffect. The browser POSTs the form natively.
The thank-you page
app/thanks/page.tsx:
import Link from 'next/link'
export default function Thanks() {
return (
<main className="mx-auto max-w-lg px-6 py-24 text-center">
<h1 className="text-4xl font-bold mb-2">Got it.</h1>
<p className="text-gray-600 mb-6">We'll reply within a day or two.</p>
<Link href="/" className="text-blue-600 underline">← Back home</Link>
</main>
)
}
Done. You are two files deep into a working contact form.
"But what about progressive enhancement and client validation?"
Good question, and here's the part people overthink. The browser already handles most of it for you. type="email" gives you validation. required blocks empty submits. The action attribute gives you a working form before React hydrates, which means your page works even if the JS bundle failed to load, which is more common than you think on flaky mobile networks.
If you want fancier UX — inline error messages, a loading spinner, no full-page redirect — you can upgrade this to a client component and fetch the endpoint yourself. I'd only do that once the basic version is live and shipping real leads. Otherwise you're pre-optimizing a page you haven't launched yet.
When I'd actually write an API route
I'm not against server code. I write a Next.js API route for contact forms when:
- I need to transform the payload before it hits the vendor (rare)
- I need to enrich from an authenticated session (also rare — usually the person filling out a contact form is logged out)
- I'm running on a platform without outbound HTTPS from the client (very rare)
That's the whole list. Everything else — email delivery, spam filtering, webhook retries, storage, export — is better handled by a tool that does only that.
The honeypot — please leave it in
The hidden website input at the bottom is the single highest-ROI thing on this page. Real humans will never touch it. Bots will fill it gleefully, and the server will drop the submission before it reaches your inbox. If you're curious why it works so well, I wrote about 30 days of logging form bots — the short version is that one honeypot field cut junk by 74% on its own.
Create a free form, swap the slug in the code above, and push. Your Next.js site now has a contact form you will never have to babysit.
Or keep writing API routes. I'm not your boss. Just keep the receipts — here's what a year of that actually cost a friend.
More posts