"does this look like an email?", not "can this address actually receive mail?" This post covers the layers of email validation and how to add the ones regex can't.
Layer 1: syntax (regex), necessary but weak
A pragmatic pattern catches obvious garbage:
const looksValid = (email) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
Don't chase the "perfect" RFC 5322 regex: it's enormous and still won't tell you the domain exists. Use a simple pattern to reject nonsense, then move on.
What regex can't tell you:
- Does the domain have a mail server? (
@asdf.asdf passes regex, accepts no mail.)
- Is it disposable? (
@mailinator.com is perfectly valid syntactically.)
- Did the user mean
gmail.com instead of gmial.com?
Layer 2: domain / MX records
A real address needs a domain with an MX (mail exchanger) record. In Node you can check DNS yourself:
import { resolveMx } from "node:dns/promises";
async function domainAcceptsMail(domain) {
try {
const records = await resolveMx(domain);
return records.length > 0;
} catch {
return false;
}
}
This already removes a big class of fakes. But it runs only server-side, doesn't cover disposable detection or typo suggestions, and you'll end up maintaining disposable-domain lists yourself.
Layer 3: disposable, role, and typo detection
This is where a verification API saves you a lot of list-maintenance and DNS plumbing. Rather than rolling it all yourself, one call returns the full picture:
npm install mailguard
import { MailGuard } from "mailguard";
const mg = new MailGuard(process.env.MAILGUARD_KEY);
const result = await mg.verify("jane@gmial.com");
// {
// status: "risky",
// score: 75,
// checks: { syntax: true, mx_found: true, disposable: false, role: false },
// did_you_mean: "gmail.com"
// }
if (await mg.isDeliverable(email)) {
// safe to accept
}
The SDK is dependency-free and works in Node 18+, Bun, Deno, Cloudflare Workers, and the browser, so the same code runs on your API or your frontend.
Putting the layers together at signup
-
On blur: call the API, show a "did you mean...?" hint if
did_you_mean is set.
-
On submit: reject
status === "undeliverable"; warn (don't hard-block) on "risky".
-
Server-side: re-check on the backend too; never trust the client alone.
app.post("/signup", async (req, res) => {
const r = await mg.verify(req.body.email);
if (r.status === "undeliverable") return res.status(400).json({ error: "Invalid email" });
// proceed to create the account
});
Summary
- Regex = "looks like an email." Keep it simple.
- MX lookup = "the domain can receive mail." Worth doing.
- Disposable/role/typo detection + a single deliverability score = the part that actually cleans your signups, and the part not worth building from scratch.
Disclosure: I build an email verification API, so this is a topic I work on daily.
The approach above is vendor-neutral; any verification API follows the same shape.