{}</>
Developer10 min readMarch 28, 2026

How to Validate Email at Signup in Node.js

A practical guide to adding real email validation to your Node.js signup flow. Goes beyond regex to cover DNS checks, disposable detection, and API-based validation with code examples.

Regex Is Not Email Validation

Every Node.js tutorial on email validation starts the same way: a regular expression. Something like /^[^\s@]+@[^\s@]+\.[^\s@]+$/ that checks for basic format. It confirms there is an @ symbol, something before it, and something after it.

This catches typos like "john@" or "@gmail.com". It does not catch anything that actually matters for signup fraud. throwaway@mailinator.com passes every regex in existence. So does xk7qm3vb9@gmail.com. So does john@gmial.com. Format validation is table stakes, not a solution.

This guide walks through four layers of email validation you can add to a Node.js signup flow, from basic format checks to API-powered fraud detection. Each layer catches threats the previous one misses.

Layer 1: Format Validation

Start with format checking, but do not write your own regex. Use a tested library instead.

import { z } from 'zod';

const signupSchema = z.object({
  email: z.string().email('Please enter a valid email address'),
  name: z.string().min(1, 'Name is required'),
});

// In your route handler
app.post('/signup', (req, res) => {
  const result = signupSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      errors: result.error.flatten().fieldErrors,
    });
  }
  // Continue with validated data
  const { email, name } = result.data;
});

Zod handles RFC-compliant email format validation and gives you typed output. If you are already using it for request validation (and you should be), this costs you one extra line.

What this catches: empty strings, missing @ signs, spaces, obviously malformed input.

What this misses: everything else. A properly formatted email can still be fake, disposable, mistyped, or malicious.

Layer 2: DNS and MX Record Checks

The next step is checking whether the domain can actually receive email. Node.js has a built-in DNS module for this.

import dns from 'dns/promises';

async function hasMxRecords(domain: string): Promise<boolean> {
  try {
    const records = await dns.resolveMx(domain);
    return records.length > 0;
  } catch {
    return false;
  }
}

// Usage
const domain = email.split('@')[1];
const canReceiveMail = await hasMxRecords(domain);

if (!canReceiveMail) {
  return res.status(400).json({
    error: 'That email domain does not appear to accept mail.',
  });
}

This is a lightweight check that catches completely bogus domains. If someone types user@notarealdomain.xyz and that domain has no MX records, you know immediately it is fake.

What this catches: nonexistent domains, domains that are not set up for email, some very obvious fakes.

What this misses: disposable email services (they have valid MX records), mistyped real domains like gmial.com (which often have MX records pointing to parked pages), and catch-all servers that accept mail for any address.

Layer 3: Disposable Domain Detection

Disposable email services like Mailinator, Guerrilla Mail, and Tempmail are the most common tool for signup fraud. Maintaining a blocklist is one approach, but the list of disposable domains grows constantly. New ones appear every week.

// A basic blocklist approach (not recommended as your only defense)
const disposableDomains = new Set([
  'mailinator.com',
  'guerrillamail.com',
  'tempmail.com',
  'throwaway.email',
  // ... hundreds more
]);

function isDisposable(email: string): boolean {
  const domain = email.split('@')[1].toLowerCase();
  return disposableDomains.has(domain);
}

The problem with a static blocklist is maintenance. BigShield tracks over 72,000 disposable domains and updates the database continuously. New disposable services launch regularly, and some rotate through subdomains to avoid detection. A static list in your codebase will always be behind.

If you want to maintain your own list, the disposable-email-domains GitHub repository is a solid open-source starting point with around 3,000 domains. But for production use where fraud has real cost, an API that stays current is worth the tradeoff.

Layer 4: API-Based Validation

This is where you get real coverage. An email validation API combines format checking, DNS verification, disposable detection, SMTP probing, pattern analysis, and reputation scoring into a single call. Instead of maintaining multiple layers yourself, you send the email and get back a verdict.

Here is how to integrate BigShield into an Express signup route:

import express from 'express';
import { BigShield } from 'bigshield';
import { z } from 'zod';

const app = express();
app.use(express.json());

const shield = new BigShield(process.env.BIGSHIELD_API_KEY!);

const signupSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

app.post('/api/signup', async (req, res) => {
  // 1. Format validation
  const parsed = signupSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({
      error: 'Invalid input',
      details: parsed.error.flatten().fieldErrors,
    });
  }

  const { email, name } = parsed.data;

  // 2. Email risk validation
  const validation = await shield.validate(email);

  if (validation.recommendation === 'reject') {
    return res.status(400).json({
      error: 'Please use a valid email address.',
      // Don't expose internal risk details to the client
    });
  }

  if (validation.recommendation === 'review') {
    // Medium risk — you could flag for manual review,
    // require additional verification, or allow with limits
    console.log(`Flagged signup: ${email} (score: ${validation.risk_score})`);
  }

  // 3. Create the account
  const user = await createUser({ email, name });

  return res.status(201).json({ id: user.id });
});

The validation call runs in under 100ms and returns a risk score from 0 to 100 plus a recommendation of accept, review, or reject. You get 30+ individual signal results if you need them, but for most signup flows, the recommendation field is all you need.

Handling Edge Cases

A few patterns that trip up naive implementations:

Timeouts. Your validation API might be down or slow. Never let email validation block signup entirely. Set a timeout and fall back to allowing the signup if the check fails.

async function validateWithFallback(email: string) {
  try {
    const result = await Promise.race([
      shield.validate(email),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('timeout')), 3000)
      ),
    ]);
    return result;
  } catch (error) {
    // Validation service unavailable or timed out — allow signup
    // but flag for async re-validation
    console.warn(`Validation failed for ${email}, allowing with flag`);
    return { recommendation: 'accept', fallback: true };
  }
}

Plus addressing. Gmail and some other providers support plus tags: john+signup1@gmail.com delivers to john@gmail.com. Some users use this legitimately for filtering. Some abusers use it to create unlimited "unique" addresses from one inbox. BigShield's email tumbling detection catches abuse patterns while allowing legitimate use.

Dot tricks. Gmail ignores dots in the local part. j.o.h.n@gmail.com and john@gmail.com are the same inbox. Abusers use this to create seemingly different addresses that all route to the same account. Normalizing the email before your uniqueness check helps, but API-based validation catches this automatically.

Case sensitivity. The local part of an email is technically case-sensitive per RFC 5321, but almost no mail server enforces this. Always lowercase the full email before storing and comparing.

const normalizedEmail = email.trim().toLowerCase();

Where to Put the Validation

Validate server-side, always. Client-side validation (like checking format in JavaScript before the form submits) is a UX convenience, not a security measure. Anyone can bypass client-side checks with a direct API call.

The ideal flow:

  1. Client side: Basic format check for instant feedback (Zod or HTML5 type="email")
  2. Server side, before account creation: Full API validation. This is the gate.
  3. Async, after signup: For addresses that passed validation but were borderline (risk score 30-50), consider a re-validation after 24 hours or a requirement to verify via confirmation email

Never validate after account creation and provisioning. If you are running an AI product that allocates tokens or compute credits on signup, every fake account that gets through costs real money. Validate first, provision second.

Testing Your Validation

You need test cases that cover the spectrum. Here is a starting set:

const testCases = [
  // Should pass
  { email: 'real.user@gmail.com', expect: 'accept' },
  { email: 'user@company.com', expect: 'accept' },

  // Should reject — disposable
  { email: 'test@mailinator.com', expect: 'reject' },
  { email: 'fake@guerrillamail.com', expect: 'reject' },

  // Should reject — invalid domain
  { email: 'nobody@thisisnotareal.domain', expect: 'reject' },

  // Should flag — typo domain
  { email: 'user@gmial.com', expect: 'review' },
  { email: 'user@yaho.com', expect: 'review' },

  // Should flag — random pattern
  { email: 'xk7qm3vb9@gmail.com', expect: 'review' },
];

BigShield's test API keys (ev_test_...) let you run these checks without burning your monthly quota. Use them in your CI pipeline to make sure validation stays wired up correctly after deploys.

Full Example: Express + BigShield + Prisma

Here is a complete signup route that ties everything together:

import express from 'express';
import { BigShield } from 'bigshield';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';

const app = express();
app.use(express.json());

const shield = new BigShield(process.env.BIGSHIELD_API_KEY!);
const prisma = new PrismaClient();

const signupSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

app.post('/api/signup', async (req, res) => {
  const parsed = signupSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({
      error: 'Invalid input.',
    });
  }

  const email = parsed.data.email.trim().toLowerCase();
  const name = parsed.data.name.trim();

  // Check for existing user first (cheap query)
  const existing = await prisma.user.findUnique({
    where: { email },
  });
  if (existing) {
    return res.status(409).json({
      error: 'An account with this email already exists.',
    });
  }

  // Validate email quality
  let validation;
  try {
    validation = await shield.validate(email);
  } catch {
    // Validation service unavailable — allow but flag
    validation = { recommendation: 'accept', risk_score: -1 };
  }

  if (validation.recommendation === 'reject') {
    return res.status(400).json({
      error: 'Please use a valid email address.',
    });
  }

  // Create the user
  const user = await prisma.user.create({
    data: {
      email,
      name,
      riskScore: validation.risk_score,
      flagged: validation.recommendation === 'review',
    },
  });

  // Send welcome email, allocate resources, etc.
  // Only AFTER validation passes

  return res.status(201).json({ id: user.id, email: user.email });
});

app.listen(3000);

This covers format validation, duplicate checking, email risk scoring, and graceful fallback if the validation service is unavailable. The risk score is stored on the user record so you can audit later.

What This Gets You

With all four layers in place, you are catching:

  • Malformed addresses (format validation)
  • Nonexistent domains (DNS/MX checks)
  • Disposable and burner emails (72,000+ domain database)
  • Bot-generated random addresses (pattern analysis)
  • Typo domains that look real but are not (typo correction)
  • Addresses on spam blocklists (DNSBL checks)

Most implementations see a 30 to 40% reduction in fake signups after adding API-based validation. For AI products that provision compute credits on signup, that translates directly to money saved.

Get started with BigShield's free tier (1,500 validations/month) or check the documentation for the full SDK reference.

Ready to stop fake signups?

BigShield validates emails with 20+ signals in under 200ms. Start for free, no credit card required.

Get Started Free

Related Articles