How to validate emails in Node.js without installing a package
Regex catches format. It misses everything else.
user@fakdomain123.com passes. user@mailinator.com passes.
admin@company.com (a role-based address you should flag) passes.
Every regex you copy from Stack Overflow has the same blind spot: it validates characters, not infrastructure.
npm packages don't solve this either. email-validator adds a dependency and still only checks format.
deep-email-validator does SMTP probing, which times out behind firewalls and gets your server IP blocklisted by mail providers.
You need three checks: syntax, MX records, and disposable provider detection. The botoi API does all three in one POST. No install. No regex file. No SMTP connection.
The API call
One curl command to see the response shape:
curl -X POST https://api.botoi.com/v1/email/validate \
-H "Content-Type: application/json" \
-d '{"email": "test@tempmail.xyz"}' The response tells you everything about the address:
{
"success": true,
"data": {
"email": "test@tempmail.xyz",
"is_valid": false,
"reason": "no_mx_records",
"is_free": false,
"is_role": false,
"is_disposable": true,
"domain": "tempmail.xyz",
"format_valid": true
}
} format_valid confirms the syntax. is_valid combines format, MX, and deliverability into a single boolean.
is_disposable flags throwaway providers. is_role catches addresses like admin@ and info@.
reason tells you why validation failed when is_valid is false.
Node.js integration: Express signup route
Here's a complete Express route handler that validates the email on POST /signup using native fetch.
The fail-open pattern ensures a botoi outage never blocks real signups:
import express from 'express';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
// Validate email before creating the account
try {
const validation = await fetch('https://api.botoi.com/v1/email/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.BOTOI_API_KEY}`,
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const result = await validation.json();
if (result.success && !result.data.is_valid) {
return res.status(422).json({
error: 'Invalid email address',
reason: result.data.reason,
});
}
} catch {
// Fail open: if the API is unreachable, let the signup proceed
console.warn('Email validation API unreachable, skipping check');
}
// Email passed validation; continue with account creation
// await createUser(email, password);
return res.status(201).json({ message: 'Account created' });
});
app.listen(3000);
The route rejects invalid emails with a 422 and the specific reason from the API.
If the API is unreachable, the signup proceeds. You trade a missed check for uninterrupted user flow.
Three-layer validation
The /v1/email/validate endpoint covers the basics. For thorough checking, combine three endpoints:
/v1/email/validatefor syntax and format/v1/email-mx/verifyfor MX record verification/v1/disposable-email/checkfor disposable detection
This helper function calls all three and returns a structured result:
interface ValidationResult {
valid: boolean;
formatValid: boolean;
mxValid: boolean;
isDisposable: boolean;
reason: string | null;
}
const API_BASE = 'https://api.botoi.com/v1';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.BOTOI_API_KEY}`,
};
async function validateEmail(email: string): Promise<ValidationResult> {
const result: ValidationResult = {
valid: true,
formatValid: false,
mxValid: false,
isDisposable: false,
reason: null,
};
// Layer 1: Syntax and format check
const formatRes = await fetch(`${API_BASE}/email/validate`, {
method: 'POST',
headers,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const formatData = await formatRes.json();
if (!formatData.success || !formatData.data.format_valid) {
return { ...result, valid: false, reason: 'invalid_format' };
}
result.formatValid = true;
// Layer 2: MX record verification
const mxRes = await fetch(`${API_BASE}/email-mx/verify`, {
method: 'POST',
headers,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const mxData = await mxRes.json();
if (!mxData.success || !mxData.data.has_mx) {
return { ...result, valid: false, reason: 'no_mx_records' };
}
result.mxValid = true;
// Layer 3: Disposable provider detection
const dispRes = await fetch(`${API_BASE}/disposable-email/check`, {
method: 'POST',
headers,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const dispData = await dispRes.json();
if (dispData.success && dispData.data.is_disposable) {
return { ...result, valid: false, isDisposable: true, reason: 'disposable_provider' };
}
return result;
} Use it like this:
const result = await validateEmail('user@tempmail.xyz');
if (!result.valid) {
console.log(`Rejected: ${result.reason}`);
// Rejected: disposable_provider
} Each layer catches a different class of bad email. Format checks stop garbage input. MX checks stop invented domains. Disposable checks stop throwaway signups. Together, they cover the gap that regex leaves open.
Adding to a Next.js API route
The same logic works in a Next.js App Router route handler at app/api/validate-email/route.ts.
This version runs the format check and disposable check in parallel to cut latency:
import { NextResponse } from 'next/server';
const API_BASE = 'https://api.botoi.com/v1';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.BOTOI_API_KEY}`,
};
export async function POST(req: Request) {
const body = await req.json();
const email = body.email?.trim().toLowerCase();
if (!email) {
return NextResponse.json(
{ error: 'Email is required' },
{ status: 400 }
);
}
try {
// Run format check and disposable check in parallel
const [formatRes, dispRes] = await Promise.all([
fetch(`${API_BASE}/email/validate`, {
method: 'POST',
headers,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
}),
fetch(`${API_BASE}/disposable-email/check`, {
method: 'POST',
headers,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
}),
]);
const [formatData, dispData] = await Promise.all([
formatRes.json(),
dispRes.json(),
]);
if (formatData.success && !formatData.data.format_valid) {
return NextResponse.json(
{ valid: false, reason: 'Invalid email format' },
{ status: 422 }
);
}
if (dispData.success && dispData.data.is_disposable) {
return NextResponse.json(
{ valid: false, reason: 'Disposable emails are not allowed' },
{ status: 422 }
);
}
// Then check MX records
const mxRes = await fetch(`${API_BASE}/email-mx/verify`, {
method: 'POST',
headers,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const mxData = await mxRes.json();
if (mxData.success && !mxData.data.has_mx) {
return NextResponse.json(
{ valid: false, reason: 'Email domain has no mail server' },
{ status: 422 }
);
}
return NextResponse.json({ valid: true });
} catch {
// Fail open on API errors
return NextResponse.json({ valid: true });
}
}
Call this route from your signup form's onBlur handler to validate the email before the user submits.
The parallel Promise.all keeps the response time under 200ms for most requests.
Handling edge cases
Three patterns trip up naive validators:
Catch-all domains
Some domains accept email at any address. anything@catch-all-domain.com won't bounce,
but the mailbox might not exist. The MX check confirms the domain has a mail server.
Confirming the specific mailbox requires sending an email, which this approach avoids on purpose.
For most signup flows, domain-level verification is enough.
Role-based addresses
Addresses like admin@, info@, and support@ are shared inboxes.
They're valid, but they're poor candidates for account ownership because no single person controls them.
The API flags these with is_role: true. You can warn the user or block signup based on your product's needs.
Plus addressing
user+tag@gmail.com delivers to user@gmail.com.
This is a legitimate feature, not abuse. But it lets one person create many accounts with the same mailbox.
If you want to prevent this, strip the + suffix before validation and store the normalized address.
Here's a function that handles all three:
async function validateEmailStrict(email: string): Promise<{
valid: boolean;
warnings: string[];
reason: string | null;
}> {
const warnings: string[] = [];
// Check for plus addressing (user+tag@gmail.com)
const localPart = email.split('@')[0];
if (localPart.includes('+')) {
warnings.push('plus_addressing');
}
const res = await fetch('https://api.botoi.com/v1/email/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.BOTOI_API_KEY}`,
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
if (!data.success) {
return { valid: false, warnings, reason: 'api_error' };
}
// Flag role-based addresses (admin@, info@, support@)
if (data.data.is_role) {
warnings.push('role_based_address');
}
// Reject disposable providers
if (data.data.is_disposable) {
return { valid: false, warnings, reason: 'disposable_provider' };
}
// Reject domains with no MX records
if (!data.data.is_valid && data.data.reason === 'no_mx_records') {
return { valid: false, warnings, reason: 'no_mx_records' };
}
return { valid: data.data.is_valid, warnings, reason: null };
} const result = await validateEmailStrict('admin+test@example.com');
// {
// valid: true,
// warnings: ['plus_addressing', 'role_based_address'],
// reason: null
// } npm packages vs. API approach
| Feature | email-validator | deep-email-validator | botoi API |
|---|---|---|---|
| Dependencies | 1 | 5+ | 0 (native fetch) |
| Format check | Yes | Yes | Yes |
| MX check | No | Yes (SMTP) | Yes (DNS) |
| Disposable detection | No | Yes (local list) | Yes (700+ domains) |
| SMTP probing | No | Yes (unreliable) | No (DNS-based) |
| Firewall safe | Yes | No | Yes |
| Maintenance | You update | You update | API updates |
The npm package approach puts the validation logic and its data (domain lists, regex patterns) in your codebase. You own the updates. When a new disposable provider launches, someone needs to open a PR to add it. The API approach offloads that maintenance. The domain list updates server-side. Your code stays the same.
The tradeoff: an API adds a network dependency. The fail-open pattern shown in the Express and Next.js examples handles this. If the API is down, validation passes and you rely on your other signup protections (email confirmation, rate limiting) until the API recovers.
Frequently asked questions
- How do I validate an email address in Node.js without installing a package?
- Use the native fetch API to call a validation endpoint like the botoi email API. Send a POST request with the email address, and the API returns format validity, MX record status, and disposable provider detection. No npm install, no regex maintenance, and no SMTP connections required.
- What is the best email validation API for Node.js?
- The best API combines three checks in one call: syntax validation, MX record verification, and disposable domain detection. The botoi API covers all three and returns results in under 100ms. It works from any Node.js version that supports fetch (18+), and the free tier handles 100 requests per day.
- How do I check if an email address exists without sending a message?
- You can verify the domain has valid MX records using a DNS-based check, which confirms the mail server exists and accepts connections. The botoi /v1/email-mx/verify endpoint does this over DNS without opening an SMTP connection, so your IP never gets blocklisted. This approach confirms the domain is real but cannot confirm whether a specific mailbox exists.
- How do I detect disposable email addresses in Node.js?
- Call the botoi /v1/disposable-email/check endpoint with the email address. It checks against 700+ known disposable providers and returns an is_disposable boolean. You can combine this with format validation and MX checking for a thorough validation pipeline, all using native fetch calls.
- Is regex enough for email validation in Node.js?
- No. Regex only checks format. An email like user@fakdomain123.com passes regex but has no mail server. user@mailinator.com passes regex but is a throwaway address. Effective validation requires checking MX records and screening disposable providers, which regex cannot do.
Try this API
Email Validation API — interactive playground and code examples
More integration posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.