Email Validation for Web Forms: Client-Side and Server-Side Techniques
On this page
Why Form-Level Email Validation Matters
Every email address that enters your database through a web form affects your data quality, deliverability and marketing ROI. Validation at the point of entry prevents bad data from ever getting in:
| Problem | Cause | Impact | Prevention |
|---|---|---|---|
| Typos | User types "gmial.com" instead of "gmail.com" | Bounced emails; lost contacts | Typo suggestion / autocorrect |
| Fake addresses | User enters "asdf@asdf.com" to bypass a required field | Inflated list; wasted sends; skewed metrics | Real-time verification API |
| Disposable addresses | User enters a throwaway address | Contact disappears within hours/days | Disposable domain detection |
| Role-based addresses | User enters "info@" or "admin@" instead of personal | Low engagement; potential spam traps | Role-based detection |
| Syntax errors | Missing @ symbol, spaces, invalid characters | Form submission errors; data corruption | Client-side validation |
| Duplicate entries | Same person signs up multiple times | Inflated counts; duplicate emails | Server-side deduplication |
Validation Layers
Email validation works best as a layered system. Each layer catches different types of errors:
| Layer | What it checks | When it runs | Catches |
|---|---|---|---|
| HTML5 built-in validation | Basic format (requires @) | On form submission | Missing @, empty field |
| Client-side JavaScript | Format, common typos, domain suggestions | On blur (field exit) or keystroke | Typos, syntax errors, obvious fakes |
| Server-side validation | Format, domain MX records, business rules | On form submission (server) | Anything client-side missed; security |
| Real-time API verification | Mailbox existence, disposable detection, risk scoring | On blur or submission | Invalid mailboxes, disposable, role-based |
| Double opt-in | Human confirmation via email | After form submission | Fake addresses, typos, consent verification |
Client-Side Validation
HTML5 built-in validation
The simplest validation is the HTML5 type="email" attribute, which browsers validate automatically:
<form>
<label for="email">Email address</label>
<input
type="email"
id="email"
name="email"
required
placeholder="you@example.com"
/>
<button type="submit">Subscribe</button>
</form>
What HTML5 type="email" checks |
What it does NOT check |
|---|---|
| Contains an @ symbol | Whether the domain exists |
| Has text before and after @ | Whether the mailbox exists |
| Prevents most obviously invalid formats | Whether it is a disposable address |
| Shows browser-native error messages | Common typos like "gmial.com" |
| Works without JavaScript | Role-based addresses |
JavaScript validation
For more control, add JavaScript validation that runs when the user leaves the email field:
function validateEmail(email) {
const errors = [];
// Basic format check
const formatRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formatRegex.test(email)) {
errors.push('Please enter a valid email address.');
return errors;
}
const [localPart, domain] = email.split('@');
// Check local part length (RFC 5321: max 64)
if (localPart.length > 64) {
errors.push(
'The part before @ is too long (max 64 characters).'
);
}
// Check domain length (RFC 5321: max 255)
if (domain.length > 255) {
errors.push(
'The domain name is too long (max 255 characters).'
);
}
// Check for common typos
const typoSuggestion = checkCommonTypos(domain);
if (typoSuggestion) {
errors.push(
`Did you mean @${typoSuggestion}?`
);
}
return errors;
}
function checkCommonTypos(domain) {
const corrections = {
'gmial.com': 'gmail.com',
'gmaill.com': 'gmail.com',
'gamil.com': 'gmail.com',
'gmal.com': 'gmail.com',
'gmail.co': 'gmail.com',
'gmail.con': 'gmail.com',
'gnail.com': 'gmail.com',
'yahooo.com': 'yahoo.com',
'yaho.com': 'yahoo.com',
'yahoo.co': 'yahoo.com',
'hotmial.com': 'hotmail.com',
'hotmal.com': 'hotmail.com',
'hotmail.co': 'hotmail.com',
'outlok.com': 'outlook.com',
'outloo.com': 'outlook.com',
'outlook.co': 'outlook.com',
};
return corrections[domain.toLowerCase()] || null;
}
Common regex patterns
| Pattern | Strictness | What it catches | Limitations |
|---|---|---|---|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/ |
Loose | No spaces; has @ and dot | Allows some technically invalid formats |
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ |
Medium | Alphanumeric + common special chars; TLD of 2+ chars | Rejects some valid addresses (plus addressing edge cases) |
| Full RFC 5322 regex | Strict | Matches the email specification precisely | Extremely complex; over 6,000 characters; not recommended |
Recommendation
Use the loose regex for client-side validation. Overly strict regex patterns reject valid email addresses (addresses with plus signs, long TLDs, internationalised domain names). Let the server-side validation and real-time API do the heavy lifting.
Server-Side Validation
Server-side validation is required regardless of client-side validation, because client-side checks can be bypassed:
Python (Flask) example
import re
import dns.resolver
def validate_email_server(email):
"""Validate email on the server side."""
errors = []
# 1. Format check
pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
if not re.match(pattern, email):
errors.append('Invalid email format.')
return errors
local_part, domain = email.rsplit('@', 1)
# 2. Length checks (RFC 5321)
if len(local_part) > 64:
errors.append('Local part exceeds 64 characters.')
if len(domain) > 255:
errors.append('Domain exceeds 255 characters.')
if len(email) > 254:
errors.append(
'Email address exceeds 254 characters.'
)
# 3. DNS MX record check
try:
mx_records = dns.resolver.resolve(
domain, 'MX'
)
if not mx_records:
errors.append(
'Domain does not accept email.'
)
except (
dns.resolver.NoAnswer,
dns.resolver.NXDOMAIN,
dns.resolver.NoNameservers,
):
errors.append('Domain does not exist.')
except Exception:
pass # DNS timeout; allow through
# 4. Disposable domain check
disposable_domains = load_disposable_domains()
if domain.lower() in disposable_domains:
errors.append(
'Please use a non-disposable email address.'
)
# 5. Role-based check
role_prefixes = [
'info', 'admin', 'support', 'sales',
'contact', 'help', 'webmaster', 'postmaster',
'noreply', 'no-reply',
]
if local_part.lower() in role_prefixes:
errors.append(
'Please use a personal email address.'
)
return errors
Node.js (Express) example
const dns = require('dns').promises;
async function validateEmailServer(email) {
const errors = [];
// 1. Format check
const formatRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formatRegex.test(email)) {
errors.push('Invalid email format.');
return errors;
}
const [localPart, domain] = email.split('@');
// 2. Length checks
if (localPart.length > 64) {
errors.push('Local part too long.');
}
if (domain.length > 255) {
errors.push('Domain too long.');
}
// 3. MX record check
try {
const mxRecords = await dns.resolveMx(domain);
if (!mxRecords || mxRecords.length === 0) {
errors.push('Domain does not accept email.');
}
} catch (err) {
if (err.code === 'ENOTFOUND') {
errors.push('Domain does not exist.');
}
// Timeout: allow through
}
return errors;
}
Real-Time API Verification
For the highest accuracy, use a third-party verification API that checks whether the mailbox actually exists:
Verification API providers
| Provider | Real-time API | Features | Pricing |
|---|---|---|---|
| ZeroBounce | Yes | Mailbox check, disposable detection, abuse scoring, AI scoring | $0.008-$0.01/verification |
| NeverBounce | Yes | Mailbox check, disposable, accept-all detection | $0.008-$0.01/verification |
| Kickbox | Yes | Mailbox check, disposable, role-based, free email detection | $0.008-$0.01/verification |
| Emailable | Yes | Mailbox check, disposable, accept-all, MX check | $0.007-$0.01/verification |
| Abstract API | Yes | Mailbox check, disposable, format, MX, SMTP | Free tier; $0.01/verification |
| Hunter.io | Yes | Mailbox check, format, domain info | $0.01/verification |
| Mailgun (verify) | Yes | Mailbox check, disposable, role-based | Included with Mailgun plans |
| Debounce | Yes | Mailbox check, disposable, role-based, catch-all | $0.005-$0.01/verification |
When to call the API
| Trigger | Pros | Cons |
|---|---|---|
| On field blur (user leaves field) | Immediate feedback; best UX | More API calls (user may edit field multiple times) |
| On form submission | Fewer API calls; validates final input | Slower submission experience |
| On field blur with debounce (300-500ms) | Balances UX and API usage | Slight delay before feedback |
| After client-side validation passes | Only calls API for well-formed emails; saves cost | Slight delay after initial validation |
UX Best Practices
| Practice | Why | Implementation |
|---|---|---|
| Show errors inline (next to the field) | Users see the error immediately | Red border + error text below field |
| Use suggestion, not rejection for typos | Reduces frustration | "Did you mean @gmail.com?" with a clickable fix |
| Validate on blur, not on every keystroke | Keystroke validation is annoying mid-typing | Add event listener to 'blur', not 'input' |
| Allow valid but unusual addresses | Plus addressing, long TLDs, hyphens are all valid | Do not over-restrict with regex |
| Show success state | Confirms the email was accepted | Green checkmark or border |
| Make error messages specific | "Invalid email" is unhelpful | "The domain 'gmial.com' doesn't exist. Did you mean gmail.com?" |
| Do not block form for DNS timeouts | DNS can be slow; do not punish the user | Allow through; verify asynchronously |
| Offer "use a different email" | If verification fails, let the user try again easily | Clear the field; refocus |
Common Validation Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Overly strict regex | Rejects valid addresses (plus signs, new TLDs, international characters) | Use loose regex; rely on MX and API checks |
| Client-side only validation | Can be bypassed entirely; no security | Always validate server-side |
| Blocking on API timeout | User cannot submit form if API is slow | Set timeout (3-5 seconds); allow through on timeout |
| Not checking MX records | Accepts addresses at non-existent domains | Add server-side MX lookup |
| Rejecting all free email providers | Blocks legitimate users | Only block if business email is truly required |
| Case-sensitive comparison | Email local parts are technically case-sensitive, but almost no mail server enforces it | Normalise to lowercase for comparison and storage |
| Not trimming whitespace | Leading/trailing spaces cause validation failure | email.trim() before validation |
| Not handling plus addressing | "user+tag@example.com" is valid | Do not strip or reject the + portion |
Extracting and Validating Email Lists from Form Submissions
When you collect email addresses through multiple web forms (signup forms, contact forms, lead magnets, checkout flows, event registrations), export the submissions and upload the files to Email Extractor to extract and deduplicate email addresses across all form sources. This consolidates contacts collected through different forms into a single clean list and removes duplicates from users who submitted multiple forms.