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
  • Email
  • 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

Frontend

React / Next.js — popular choices include:

  • React Hook Form
  • Zod validation
  • Server Actions
  • Tailwind CSS

Vue / Nuxt — common patterns:

  • useFetch()
  • server/api routes
  • 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

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:

  • name
  • email
  • phone
  • subject
  • message

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:

FieldTypeDescription
spam_score0–100Composite AI risk score
is_spambooleanWhether to block the submission
reasonstringPlain-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.