Almost every signup form on the web does the same thing: it runs a regex against the email field, accepts if it matches, rejects if not. That catches typos but lets through disposable, role-based, and non-existent addresses. Here is how to do real validation in a JavaScript signup form.
Why regex is not enough
The classic regex check looks like this:
const isValid = /^[\w\.-]+@[\w\.-]+\.\w+$/.test(email);
It passes fake@mailinator.com. It passes doesntexist@acme.com. It passes any catch-all. All you have confirmed is that the user typed an at-sign and a dot.
The three-step pattern
Real validation runs in three stages:
- Frontend regex on blur or keystroke. Catches typos as the user types.
- Frontend disposable-domain quick check (optional, gives instant feedback for obvious bad domains).
- Backend full verification on form submit before account creation.
Step 1: The frontend pattern
const emailInput = document.querySelector('input[name=email]');
const submit = document.querySelector('button[type=submit]');
const errorEl = document.querySelector('.email-error');
emailInput.addEventListener('blur', () => {
const value = emailInput.value.trim().toLowerCase();
const re = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!re.test(value)) {
errorEl.textContent = 'Please enter a valid email address.';
return;
}
errorEl.textContent = '';
});
This handles the obvious bad inputs without any network call. Fast, free, blocks typos in real time.
Step 2: The backend verification
When the user submits, your backend calls the verification API before creating the account.
// server-side, Node.js example
app.post('/signup', async (req, res) => {
const email = req.body.email.trim().toLowerCase();
const verifyRes = await fetch('https://mailoclean.com/api/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MAILOCLEAN_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await verifyRes.json();
if (data.status === 'invalid' || data.status === 'disposable') {
return res.status(422).json({
error: 'Please use a valid, permanent email address.'
});
}
// Create the account
const user = await createUser({ email, ...req.body });
return res.json({ user });
});
What to do with each status
valid: create the account.invalid: reject with a clear error message.disposable: reject and ask for a permanent email.catch_all: accept but flag for engagement monitoring.role_based: accept, optionally with a soft warning ("we recommend using a personal email").unknown: accept by default. The verifier could not reach the server; do not lock real users out for a network blip.
UX tips
- Show the spinner on the submit button while verification runs. It typically takes 1 to 2 seconds.
- Never validate on every keystroke. Validate on blur (frontend) and on submit (backend).
- Always show a friendly error, not a technical one. "Please use a different email" beats "verifier returned status: disposable".
- Let users override. If they insist their address is real, accept with a flag, and chase deliverability with engagement data.
FAQ
Can I do verification entirely in the browser?
No. The API key would be exposed to anyone viewing the page. Always proxy through your backend.
How fast is the verification call?
Median 1.4 seconds. Add an obvious loading state and users will not notice.
What if MailoClean is down?
The MailoClean API has 99.9%+ uptime, but build for the rare exception: timeout the verification at 5 seconds and accept the signup with a "needs reverify" flag, then reverify in a background job within the hour.
Ship validation that actually works
Grab an API key and copy the snippet above. Five minutes from copy to production.