If you've ever shipped a signup form and watched your bounce rate creep up a week later, you already know the problem: not every email address that looks valid is actually deliverable.
I ran into this on a transactional email pipeline — password resets, order confirmations, that kind of thing. Everything passed basic form validation, but a chunk of emails still bounced. Turns out "looks like an email" and "will actually receive an email" are two very different checks.
Here's how I fixed it, step by step.
Step 1: Why Regex Alone Isn't Enough
Most of us start here:
function isValidEmailFormat(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
console.log(isValidEmailFormat("hello@example.com")); // true
console.log(isValidEmailFormat("hello@fake-domain-xyz123.com")); // also true!
Both of those return true. The problem is regex only checks shape, not existence. fake-domain-xyz123.com might not even resolve to a real mail server. Regex will happily pass it anyway.
Step 2: Checking the Domain Actually Has Mail Servers
The next layer most people add is an MX (Mail Exchange) record lookup — this confirms the domain is actually configured to receive email:
const dns = require('dns').promises;
async function domainHasMailServer(email) {
const domain = email.split('@')[1];
try {
const mxRecords = await dns.resolveMx(domain);
return mxRecords.length > 0;
} catch (err) {
return false; // no MX records = domain can't receive mail
}
}
(async () => {
console.log(await domainHasMailServer("hello@gmail.com")); // true
console.log(await domainHasMailServer("hello@fake-domain-xyz123.com")); // false
})();
This catches a lot more junk than regex alone. Combine both checks and you've got a reasonable first line of defense:
async function basicEmailCheck(email) {
if (!isValidEmailFormat(email)) return { valid: false, reason: 'bad_format' };
const hasMx = await domainHasMailServer(email);
if (!hasMx) return { valid: false, reason: 'no_mail_server' };
return { valid: true };
}
Step 3: Where This Approach Breaks Down
MX-record checking is a solid start, but it has real limits once you're running this in production:
- It doesn't catch full mailboxes or deactivated accounts. The domain can receive mail; that specific inbox might not exist anymore.
- Catch-all domains lie to you. Some mail servers accept any address at their domain, even ones that don't exist, then silently drop it. Your MX check says "valid," reality says otherwise.
- DNS lookups add latency if you're doing this synchronously on every signup request, and you'll want caching, retries, and rate-limit handling once volume grows.
- No disposable-email detection. Temp-mail domains often have valid MX records too.
I hit all four of these within a couple months of shipping the DIY version. At that point, rolling your own becomes a maintenance project instead of a five-line function.
Step 4: Handing the Hard Part to an API
This is where a dedicated email validation API tools like Gamalogic earns its keep — it handles MX checks, catch-all detection, disposable-domain lists, and mailbox-level verification behind one call, so you're not maintaining that logic yourself.
I ended up wiring Gamalogic into this same signup flow, since it covers the catch-all and disposable-domain gaps that MX-only checking misses:
const axios = require('axios');
async function validateWithAPI(email, apiKey) {
const response = await axios.get('https://api.gamalogic.com/v1/verify', {
params: { email, api_key: apiKey }
});
return response.data; // { status: 'valid' | 'invalid' | 'catch-all', ... }
}
Swap this in wherever your DIY check was, and you get mailbox-level confidence instead of just "the domain exists."
Step 5: Putting It in the Signup Flow
app.post('/signup', async (req, res) => {
const { email } = req.body;
const result = await validateWithAPI(email, process.env.EMAIL_API_KEY);
if (result.status === 'invalid') {
return res.status(400).json({ error: 'This email address doesn\'t look deliverable.' });
}
// proceed with account creation
res.status(200).json({ message: 'Signed up successfully' });
});
Reject clearly invalid addresses at signup, and you avoid the downstream cost entirely — bounced password resets, wasted sends, and a slowly-tanking sender reputation.
Takeaway
Regex gets you shape. MX lookups get you domain existence. Neither gets you mailbox-level confidence, and that gap is exactly where bounce rates come from. If you're just prototyping, the DIY checks above are fine. If you're sending real transactional volume, it's worth handing that last mile to something built for it rather than re-discovering catch-all domains the hard way, like I did.
Top comments (0)