How to Build a Modern Contact Form for Your Website
React, Next.js, Vue & Serverless Best Practices — a complete guide to building contact forms with proper validation, clean architecture, and good UX.
Modern websites are evolving fast. Today, many are built using AI-assisted tools, serverless platforms, and modern frontend frameworks like Next.js, React, and Vue.js.
Whether you are building a landing page, SaaS dashboard, portfolio, or startup website, one thing almost every project still needs is a contact form.
At first glance, contact forms seem simple:
- Collect a few fields
- Send an email
- Show a success message
But modern contact forms are actually small public APIs exposed to the internet. That means they should be built carefully — with proper validation, clean architecture, and a good user experience.
In this guide, we will walk through how modern contact forms are commonly built today, and how to improve them with better validation and submission flows.
What a Modern Contact Form Usually Includes
Most modern contact forms contain:
- Name
- Phone
- Subject
- Message
And behind the scenes:
- Frontend form UI
- API endpoint or server action
- Email provider or database
- Validation layer
- Success/error handling
Common Contact Form Architecture
A typical modern setup looks like this:
Frontend Form
↓
API Endpoint / Serverless Function
↓
Validation
↓
Email Provider / Database
↓
Success Response
This architecture is commonly used in:
- Next.js applications
- Vue/Nuxt apps
- Cloudflare Workers
- Vercel serverless deployments
- AI-generated web projects
Recommended Stack
Frontend
React / Next.js — popular choices include:
- React Hook Form
- Zod validation
- Server Actions
- Tailwind CSS
Vue / Nuxt — common patterns:
useFetch()server/apiroutes- Composables
Backend & Email Services
Many developers use:
- Resend
- SendGrid
- Postmark
- Supabase
- Firebase
Example: Basic React Contact Form
Here is a simple example using React:
'use client'
import { useState } from 'react'
export default function ContactForm() {
const [loading, setLoading] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError('')
const formData = {
name: (e.currentTarget.elements.namedItem('name') as HTMLInputElement).value,
email: (e.currentTarget.elements.namedItem('email') as HTMLInputElement).value,
phone: (e.currentTarget.elements.namedItem('phone') as HTMLInputElement).value,
subject: (e.currentTarget.elements.namedItem('subject') as HTMLInputElement).value,
message: (e.currentTarget.elements.namedItem('message') as HTMLTextAreaElement).value,
}
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
})
setLoading(false)
if (response.ok) setSuccess(true)
else setError('Something went wrong. Please try again.')
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<input name="phone" placeholder="Phone" />
<input name="subject" placeholder="Subject" />
<textarea name="message" placeholder="Message" required />
<button disabled={loading}>
{loading ? 'Sending...' : 'Send Message'}
</button>
{success && <p>Message sent successfully.</p>}
{error && <div className="error-message">{error}</div>}
</form>
)
}
Example: Backend API Route
In a modern serverless app, the backend endpoint often looks like this:
export async function POST(req: Request) {
const data = await req.json()
// send email, save to DB, trigger automations...
console.log(data)
return Response.json({ success: true })
}
This endpoint can:
- Send emails
- Save leads
- Trigger automations
- Forward data to a CRM
Why Validation Matters
Many contact forms work technically, but still lack a proper validation flow.
A modern form should:
- Validate inputs
- Check content before processing
- Avoid sending unnecessary fields
- Handle errors gracefully
- Only reach the success state after validation passes
Recommended Pattern: Validate Before Success
Instead of immediately showing a success message after submit, a better approach is:
User submits form
↓
Extract text-based fields
↓
Validate submission
↓
If valid → continue processing
If invalid → show error, preserve form values
↓
Process form (email / DB / CRM)
↓
Show success state
This creates cleaner processing, better reliability, better UX, and more maintainable code.
Only Send Relevant Text Fields
One important best practice is to only validate fields that actually matter.
Recommended fields to validate:
nameemailphonesubjectmessage
Usually unnecessary:
- Dropdown UI states
- Styling metadata
- Frontend-only values
- Visual component states
This helps reduce noise, simplify validation, improve privacy, and reduce payload size.
Example: Preparing a Validation Payload
const validationPayload = {
fields: {
name: formData.name,
email: formData.email,
phone: formData.phone,
subject: formData.subject,
message: formData.message,
},
client_ip: req.headers.get('x-forwarded-for') ?? '0.0.0.0',
domain: 'yoursite.com',
}
Example: Validate Before Processing
Before moving to the success state, your backend can run the submission through a spam and content check:
const checkResponse = await fetch('https://api.inputgate.cloud/v1/check', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.INPUTGATE_API_KEY}`,
},
body: JSON.stringify(validationPayload),
})
const result = await checkResponse.json()
if (result.is_spam) {
return Response.json(
{ error: 'Your message could not be submitted.' },
{ status: 400 }
)
}
The response includes:
| Field | Type | Description |
|---|---|---|
spam_score | 0–100 | Composite AI risk score |
is_spam | boolean | Whether to block the submission |
reason | string | Plain-English explanation, safe to log |
Only after the check passes should you continue to email sending or database writes.
Continue Only After Validation Passes
// Validation passed — safe to process
await sendEmail(formData)
await saveToDatabase(formData)
return Response.json({ success: true })
This creates a cleaner and more predictable form flow — and means spam, bots, and prompt injection attempts never reach your email inbox or CRM.
Better User Experience Patterns
A good contact form is not only about backend logic. Small UX improvements make a big difference.
Recommended practices:
- Disable the submit button while loading
- Show inline validation messages
- Preserve form values after errors
- Avoid generic error messages (“Something went wrong”)
- Do not clear the form too early
- Keep loading states clear and predictable
Final Thoughts
Modern contact forms are no longer just simple email senders. They are public-facing endpoints connected to APIs, automations, databases, and serverless infrastructure.
Building them with proper validation, clean architecture, thoughtful UX, and structured processing flows helps create more reliable and maintainable web applications — and keeps your inbox free from spam, bots, and junk data.
For projects using modern serverless stacks, InputGate can validate and score form submissions before you process them — with no CAPTCHA shown to your users.