Guide · Node.js · SDK · REST API

Email Validation in Node.js — Beyond Regex

A regex can check the format, but it cannot tell you whether the domain accepts email, whether the address is disposable, or whether it will actually deliver. Here's the full picture.

1. The three layers of email validation

Email validation is not a single check — it is a stack of progressively stricter tests, each catching a different class of problem:

1

Format check (syntax)

Does the string look like an email? Local part, @, domain, TLD. Fast, no network call.

Free — run always

2

MX record check (domain deliverability)

Does the domain have mail exchange records? A format-valid email on a domain with no MX will always bounce.

~50–200ms DNS lookup

3

SMTP handshake (mailbox existence)

Does the specific mailbox exist? Connects to the mail server and checks without sending. Many providers block this.

Seconds, often blocked

Most applications need layers 1 and 2. Layer 3 (SMTP verification) is unreliable because large providers like Google and Microsoft reject or lie during SMTP handshakes to prevent email harvesting. The IsValid Email API handles layers 1 and 2.


2. The regex problem

The most common mistake is using a regex that is either too strict (rejecting valid addresses) or too loose (accepting invalid ones). RFC 5321 and 5322 define the email format, and the full spec is surprisingly permissive.

// ❌ Too strict — rejects legitimate addresses
const SIMPLE_REGEX = /^[a-z0-9.]+@[a-z0-9]+\.[a-z]{2,}$/i;

SIMPLE_REGEX.test('user+tag@example.com');   // false — + is valid
SIMPLE_REGEX.test('"john doe"@example.com'); // false — quoted local part is valid
SIMPLE_REGEX.test('user@sub.domain.co.uk'); // false — multiple dots in domain
SIMPLE_REGEX.test('user@xn--nxasmq6b.com'); // false — IDN domains are valid

// ❌ Too loose — accepts clearly invalid addresses
const LOOSE_REGEX = /^.+@.+$/;
LOOSE_REGEX.test('not-an-email');     // false (ok here)
LOOSE_REGEX.test('@domain.com');      // true  ✗ — no local part
LOOSE_REGEX.test('user@');            // true  ✗ — no domain
LOOSE_REGEX.test('user@domain');      // true  ✗ — no TLD (RFC says it's optional but rarely valid)
⚠️The HTML5 <input type="email"> regex is intentionally a simplified approximation (RFC 5322). It is a good first gate but rejects some valid addresses — most notably those with quoted local parts.

Addresses that are technically valid per RFC but almost never used in practice:

AddressWhy it's valid
"very.unusual address"@example.comQuoted local part allows spaces
user@[192.168.1.1]IP address literal as domain
user+label@example.comPlus addressing — widely used for filtering
Aaäöü@example.comInternationalised local part (RFC 6531)
user@münchen.deInternationalised domain (IDN)
ℹ️The IsValid API uses a pragmatic, simplified regex that intentionally rejects quoted local parts ("very.unusual address"@example.com), IP address literals (user@[192.168.1.1]), and internationalised local parts — these are technically RFC-valid but virtually never encountered in real-world email systems.

3. MX record lookup — does the domain accept email?

An MX (Mail Exchanger) record is a DNS entry that tells the internet which servers handle email for a domain. If a domain has no MX records, email sent there will bounce — even if the address has a perfect format.

Common failure modes

  • Typo in domainuser@gmial.com formats correctly, passes regex, but gmial.com has no MX record.
  • Expired domain — a company lets its domain lapse; old email addresses format-validate but no longer deliver.
  • New TLD with no setupuser@company.io is valid but the domain may not have email configured even if the website works.

DIY MX lookup in Node.js

import { promises as dns } from 'dns';

async function hasMxRecord(domain) {
  try {
    const records = await dns.resolveMx(domain);
    return records.length > 0;
  } catch {
    return false; // NXDOMAIN or no MX record
  }
}

console.log(await hasMxRecord('gmail.com'));   // true
console.log(await hasMxRecord('gmial.com'));   // false — typo
console.log(await hasMxRecord('example.com')); // false — reserved, no MX

This works but adds latency to every form submission. Cache results per domain to avoid repeated DNS lookups for the same domain.


4. Disposable addresses and why they matter

Services like Mailinator, Temp-Mail, Guerrilla Mail, and hundreds of others provide temporary email addresses that expire after minutes or hours. These addresses are format-valid, often have valid MX records, and will deliver email — but they are used to bypass registration requirements and will never be owned by a real user long-term.

Why users use them

  • Avoid marketing spam
  • One-time coupon codes
  • Trial account bypasses
  • Testing your own forms

Why you may want to block them

  • Invalid for long-term contact
  • Used for abuse / promo fraud
  • Inflate registration numbers
  • No GDPR-compliant contact basis
ℹ️Blocking disposable domains is a policy decision, not a technical one. For developer tools and API services, blocking them may frustrate legitimate users exploring your product. For e-commerce or SaaS with real financial transactions, blocking is usually worth the tradeoff.

5. The production-ready solution

The IsValid Email API handles RFC 5322 format validation in a single request. With the optional checkMx=true parameter it also performs a live DNS MX lookup and returns the result alongside the parsed local part and domain.

<10ms
Format check
no network call
50–200ms
MX check
live DNS lookup
100/day
Free tier
no credit card

Full parameter reference and response schema: Email Validation API docs →


6. Node.js code example

Using the IsValid SDK or the native fetch API.

import { createClient } from '@isvalid-dev/sdk';

const iv = createClient({ apiKey: process.env.ISVALID_API_KEY });

// ── Example usage ────────────────────────────────────────────────────────────

// Format check only (fast)
const result = await iv.email('alice@example.com');
console.log(result.valid);   // true
console.log(result.local);   // 'alice'
console.log(result.domain);  // 'example.com'

// With MX check (confirms domain receives email)
const withMx = await iv.email('alice@gmial.com', { checkMx: true });
console.log(withMx.valid);    // true  — format is valid
console.log(withMx.mxValid);  // false — gmial.com has no MX record

In a registration handler — validate format immediately, check MX on submit:

// routes/auth.js (Express)
app.post('/register', async (req, res) => {
  const { email, password } = req.body;

  let check;
  try {
    check = await validateEmail(email, { checkMx: true });
  } catch {
    return res.status(502).json({ error: 'Email validation service unavailable' });
  }

  if (!check.valid) {
    return res.status(400).json({ error: 'Invalid email format' });
  }

  if (check.mxValid === false) {
    return res.status(400).json({
      error: 'Email domain does not appear to accept mail. Check for typos.',
    });
  }

  // Proceed with account creation
  await createUser({ email, password });
  res.json({ success: true });
});
Run the format check on every keystroke (debounced) to give instant feedback. Trigger the MX check only on form submit — it adds 50–200ms of latency that is imperceptible at submit time but annoying during typing.

7. cURL example

Format check only (fast, no DNS):

curl -G -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "value=alice@example.com" \
  "https://api.isvalid.dev/v0/email"

With MX record lookup:

curl -G -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "value=alice@gmial.com" \
  "https://api.isvalid.dev/v0/email?checkMx=true"

Test with a plus-addressed email:

curl -G -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "value=alice+newsletter@gmail.com" \
  "https://api.isvalid.dev/v0/email?checkMx=true"

8. Understanding the response

Format valid, MX check requested and passed:

{
  "valid": true,
  "local": "alice",
  "domain": "gmail.com",
  "mxValid": true
}

Format valid, domain has no MX records (likely a typo):

{
  "valid": true,
  "local": "alice",
  "domain": "gmial.com",
  "mxValid": false
}

Format invalid:

{
  "valid": false
}
FieldTypeDescription
validbooleanRFC 5322 format is correct
localstringEverything before the @ sign
domainstringEverything after the @ sign
mxValidbooleanPresent only when checkMx=true. true if at least one MX record was found for the domain.

9. Edge cases and UX patterns

Suggest corrections for common typos

When mxValid is false, show the user a helpful message rather than a generic error. If you detect a domain that looks like a typo of a popular provider, suggest the correction.

const TYPOS = {
  'gmial.com': 'gmail.com', 'gmal.com': 'gmail.com',
  'yahooo.com': 'yahoo.com', 'hotmial.com': 'hotmail.com',
  'outlok.com': 'outlook.com',
};

function suggestCorrection(domain) {
  return TYPOS[domain.toLowerCase()] ?? null;
}

if (!check.mxValid) {
  const suggestion = suggestCorrection(check.domain);
  const msg = suggestion
    ? `Did you mean ${check.local}@${suggestion}?`
    : 'This email domain does not appear to accept mail. Please check for typos.';
  showError(msg);
}

Plus addressing (+tag) and sub-addressing

alice+tag@gmail.com is a valid address that delivers to alice@gmail.com. Many users use this for filtering. Do not reject plus-addressed emails — they are legitimate. Normalising them (stripping the tag) is a policy choice with privacy implications.

Case sensitivity

Per RFC 5321, the local part of an email is technically case-sensitive — Alice@example.com and alice@example.com are different addresses. In practice, virtually all mail servers treat the local part as case-insensitive. Lowercase the local part before storing to avoid duplicate accounts.

Never block on MX failure in all contexts

MX failures can be transient (DNS outage, propagation delay for a new domain). Consider showing a soft warning rather than a hard block, especially for B2B customers who might be entering a newly set-up company domain. The decision to hard-block depends on your fraud risk appetite.


Summary

Do not rely on a single catch-all regex — it will reject valid addresses
Do not block on MX failure alone — it can be transient
Run format check on every keystroke (debounced) for instant UX
Run MX check on submit to catch domain typos before they bounce
Suggest corrections for common provider typos (gmial → gmail)
Lowercase and store the canonical form of the address

Node.js integration notes

email address validation in Node.js sits at one of the most common entry points in any web application: user-facing forms, API request bodies, and webhook payloads. Using a branded TypeScript type — type EmailAddress = string & { readonly __brand: 'EmailAddress' } — ensures that only values that have passed through the IsValid check can flow into downstream logic. The TypeScript compiler then enforces this boundary without any runtime overhead beyond the initial validation call.

The Node.js ecosystem offers several complementary packages for working with email address values once they are validated. For email, pair with nodemailer or @sendgrid/mail; for phone numbers, use libphonenumber-js for formatting; for URLs and domains, the built-in URL class handles parsing after the validity check passes. In each case, validation with IsValid acts as the gate that ensures the downstream library receives well-formed input.

Express.js and Fastify middleware

Add email address validation as a route-level middleware in Express or a preHandler hook in Fastify. The middleware validates the incoming value, attaches the result to req.validated, and calls next() on success or returns a 400 response on failure. This keeps validation logic out of route handlers and makes it easy to apply the same check across multiple routes. For high-traffic endpoints, cache previously validated values in a Map or Redis with a short TTL.

In a Next.js API route or App Router server action, call the IsValid API inside a try/catch block. Distinguish between a 422 response (the input is invalid — return this error to the user) and network or 5xx errors (transient failures — retry once, then return a generic service-unavailable response). Never swallow validation errors silently, as they indicate bad data that could propagate further into your system.

  • Normalise email address values before validation: trim whitespace and convert to lowercase where the format is case-insensitive
  • Use Promise.allSettled() for bulk validation — it captures all results without short-circuiting on the first failure
  • In Jest tests, mock the IsValid client at the module level to keep tests fast and offline-capable
  • Store the validated value alongside the full API response in your database — normalised forms and parsed fields save work in downstream queries

When making HTTP calls to the IsValid API directly (without the SDK), the choice between fetch and axios is largely a matter of preference. The native fetch API is available in Node.js 18+ without any additional dependency and is sufficient for simple request/response flows. axios adds automatic JSON parsing, request/response interceptors, and a cleaner timeout API (axios.create({ timeout: 5000 })), which makes it easier to centralise the Authorization header and retry logic in one place. For high-throughput services that make many concurrent API calls, consider undici — the HTTP client underlying Node.js fetch — used directly for its connection pooling and lower overhead.

See also

Validate email addresses instantly

Free tier includes 100 API calls per day. No credit card required. Format check under 10ms, optional MX lookup under 200ms.