Валидация email на Node.js: библиотеки, сервисы и best practices
Форма регистрации принимает test@@gmial.con без единого возражения. Через неделю bounce rate переваливает за 5%, ESP понижает репутацию домена, и письма к реальным пользователям уходят в спам. Знакомая история. В этом руководстве разбираем, как организовать валидацию email в Node.js-приложении: от npm-пакетов для синтаксической проверки до SMTP-верификации через внешний API.
Четыре уровня проверки email
Валидация email - не одна операция, а конвейер из нескольких этапов. Каждый следующий точнее предыдущего, но требует больше ресурсов.
1. Синтаксис. Есть ли @, не пустой ли домен, нет ли пробелов. Работает за микросекунды. Отсекает очевидный мусор: пустые строки, случайный ввод, адреса без домена.
2. MX-запрос. Проверяем DNS: если у домена нет MX-записи, он не принимает почту. 50-200 мс, и вы уже знаете: отправлять сюда бессмысленно.
3. SMTP-хэндшейк. Подключение к почтовому серверу, команда RCPT TO, анализ ответа. Единственный способ проверить, существует ли конкретный ящик.
4. AI-скоринг и репутация. Определение одноразовых адресов, спам-ловушек, catch-all доменов. Для этого нужны данные, которых у локальной библиотеки нет. Здесь подключаются внешние сервисы.
В npm-экосистеме есть пакеты для первых трёх уровней. Четвёртый уровень закрывается API. Разберём каждый вариант с рабочим кодом.
email-validator: быстрая синтаксическая проверка
Пакет email-validator - самый популярный выбор для синтаксической валидации. Без зависимостей, без сетевых запросов, работает и на сервере, и в браузере.
npm install email-validatorimport * as EmailValidator from 'email-validator';
EmailValidator.validate('user@example.com'); // true
EmailValidator.validate('user@.com'); // false
EmailValidator.validate('user@@example.com'); // false
EmailValidator.validate('user@example'); // falseПод капотом - регулярное выражение, совместимое с основными правилами RFC 5322. Пакет не делает DNS-запросов и не проверяет, существует ли ящик. Его задача - убедиться, что строка выглядит как email-адрес. Для первого уровня валидации этого достаточно.
Типичное применение: middleware в Express или Fastify, который отклоняет запрос до того, как данные попадут в бизнес-логику.
import * as EmailValidator from 'email-validator';
function validateEmailMiddleware(req, res, next) {
const { email } = req.body;
if (!email || typeof email !== 'string') {
return res.status(400).json({ error: 'Email обязателен' });
}
const trimmed = email.trim().toLowerCase();
if (!EmailValidator.validate(trimmed)) {
return res.status(400).json({ error: 'Некорректный формат email' });
}
req.body.email = trimmed;
next();
}
// Использование
app.post('/api/signup', validateEmailMiddleware, signupHandler);deep-email-validator: синтаксис + MX + SMTP
Если нужно больше, чем проверка формата, есть deep-email-validator. Этот пакет последовательно проверяет синтаксис, MX-записи домена, наличие в списке одноразовых доменов и даже пробует SMTP-соединение.
npm install deep-email-validatorimport { validate } from 'deep-email-validator';
async function checkEmail(email) {
const result = await validate({
email,
validateRegex: true,
validateMx: true,
validateTypo: true,
validateDisposable: true,
validateSMTP: true,
});
return result;
}
const result = await checkEmail('user@example.com');
console.log(result.valid); // true | false
console.log(result.reason); // "smtp" | "disposable" | "mx" | ...
console.log(result.validators);
// {
// regex: { valid: true },
// typo: { valid: true },
// disposable: { valid: true },
// mx: { valid: true, mxRecords: [...] },
// smtp: { valid: true }
// }Пакет полезен для прототипов и внутренних инструментов. Но в продакшене у него есть ограничения, которые нужно учитывать.
SMTP-проверка ненадёжна с общего хостинга. Большинство почтовых серверов блокируют или ограничивают SMTP-соединения с IP-адресов облачных провайдеров (AWS, GCP, DigitalOcean). Если ваш сервер работает на таком хостинге, SMTP-валидация будет возвращать ложные отрицательные результаты.
Список disposable-доменов устаревает. Пакет хранит статичный список одноразовых почтовых сервисов. Новые сервисы появляются каждую неделю, и между обновлениями пакета они проходят валидацию.
Нет данных о catch-all и спам-ловушках. Catch-all домен отвечает 250 OK на любой адрес. SMTP-проверка здесь бесполезна. Для определения catch-all нужен агрегированный опыт тысяч проверок, которого у локального пакета нет.
Ручная проверка MX через dns.promises
Если вам не нужен весь функционал deep-email-validator, MX-проверку можно реализовать самостоятельно. Node.js включает модуль dns с поддержкой промисов. Никаких внешних зависимостей.
import dns from 'node:dns/promises';
async function hasMxRecords(email) {
const domain = email.split('@')[1];
if (!domain) return false;
try {
const records = await dns.resolveMx(domain);
return records.length > 0;
} catch {
// ENOTFOUND, ENODATA — домен не существует или нет MX
return false;
}
}
await hasMxRecords('user@gmail.com'); // true
await hasMxRecords('user@fakesite.xyz'); // false (скорее всего)Комбинация email-validator для синтаксиса и dns.resolveMx для MX - лёгкий и контролируемый вариант для второго уровня. Работает без внешних сервисов и сторонних зависимостей. Для многих проектов этого хватит в качестве первой линии защиты.
Валидация через Zod и другие schema-библиотеки
Если в проекте уже используется Zod для валидации входных данных, отдельный пакет для email может быть избыточным. Zod умеет проверять формат email из коробки.
import { z } from 'zod';
const SignupSchema = z.object({
email: z
.string()
.trim()
.toLowerCase()
.email('Некорректный формат email'),
password: z.string().min(8, 'Минимум 8 символов'),
});
// Express handler
app.post('/api/signup', (req, res) => {
const parsed = SignupSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
errors: parsed.error.flatten().fieldErrors,
});
}
// parsed.data.email уже нормализован
createUser(parsed.data);
});Аналогичный подход работает с Yup, Joi, ArkType. Schema-библиотеки решают ту же задачу, что и email-validator, но в рамках общей системы валидации. Выбирайте то, что уже есть в проекте, а не добавляйте ещё одну зависимость.
Где npm-пакеты заканчиваются
Все перечисленные библиотеки работают с тем, что доступно локально: строковый анализ, DNS, попытка SMTP-подключения. Этого хватает, чтобы отсеять 60-70% мусора. Оставшиеся 30-40% проблемных адресов проходят проверку и попадают в базу.
Catch-all домены. Сервер отвечает 250 OK на любой адрес. SMTP-проверка бесполезна. Catch-all настроено у 15-20% корпоративных доменов. Для определения реальности адреса на таком домене нужна статистика, которой у локальной библиотеки нет.
Новые одноразовые домены. Статичные списки устаревают за дни. Сервис, который появился вчера, пройдёт проверку по любому списку из npm.
Спам-ловушки. Адрес xk7q9mz2@gmail.com синтаксически валиден, MX на месте, SMTP отвечает. Но отправка на него уничтожает репутацию домена. Regex не видит разницы между этим адресом и настоящим.
IP-блокировки SMTP. Облачные провайдеры попадают в чёрные списки почтовых серверов. SMTP-проверка с AWS EC2 или GCP Compute Engine часто возвращает ложный отказ. Специализированные сервисы используют ротацию IP и установившуюся репутацию, обходя эту проблему.
Внешний API: полная проверка за один запрос
Сервисы валидации email решают перечисленные проблемы за счёт агрегированных данных, инфраструктуры для SMTP-проверок и ML-моделей для определения рисков. Один HTTP-запрос возвращает результат по всем четырём уровням.
Пример интеграции с uChecker API на Node.js:
const UCHECKER_API = 'https://api.uchecker.net/api/v1/validate/single';
async function validateEmail(email) {
const response = await fetch(UCHECKER_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.UCHECKER_API_KEY,
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
const result = await validateEmail('user@example.com');
// {
// result: "deliverable", // deliverable | undeliverable | risky | unknown
// disposable: false,
// catch_all: false,
// reason: null
// }На выходе - не бинарное «валиден/невалиден», а детальный результат: статус доставляемости, флаг одноразового адреса, признак catch-all, причина отклонения. Решение о том, принять или отклонить адрес, остаётся за вами.
Express middleware: локальная + API проверка
Рабочий паттерн: быстрая синтаксическая проверка отсекает мусор мгновенно, а адреса, прошедшие первый фильтр, уходят на API-валидацию. Два уровня в одном middleware.
import * as EmailValidator from 'email-validator';
async function emailValidationMiddleware(req, res, next) {
const { email } = req.body;
if (!email || typeof email !== 'string') {
return res.status(400).json({ error: 'Email обязателен' });
}
const normalized = email.trim().toLowerCase();
// Уровень 1: синтаксис (мгновенно)
if (!EmailValidator.validate(normalized)) {
return res.status(400).json({
error: 'Некорректный формат email',
code: 'INVALID_FORMAT',
});
}
// Уровень 2-4: API-валидация
try {
const apiResult = await fetch(
'https://api.uchecker.net/api/v1/validate/single',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.UCHECKER_API_KEY,
},
body: JSON.stringify({ email: normalized }),
signal: AbortSignal.timeout(5000), // таймаут 5 секунд
}
);
const data = await apiResult.json();
if (data.result === 'undeliverable') {
return res.status(400).json({
error: 'Email не существует',
code: 'UNDELIVERABLE',
});
}
if (data.disposable) {
return res.status(400).json({
error: 'Одноразовые email не принимаются',
code: 'DISPOSABLE',
});
}
// Сохраняем данные валидации для бизнес-логики
req.emailValidation = {
email: normalized,
result: data.result,
catchAll: data.catch_all,
disposable: data.disposable,
};
} catch (err) {
// API недоступен — пропускаем, проверим позже
console.error('Email validation API error:', err.message);
req.emailValidation = {
email: normalized,
result: 'unchecked',
catchAll: null,
disposable: null,
};
}
req.body.email = normalized;
next();
}
app.post('/api/signup', emailValidationMiddleware, signupHandler);Обратите внимание на AbortSignal.timeout(5000). Если API не отвечает за 5 секунд, middleware пропускает адрес с пометкой unchecked. Регистрация не ломается. Непроверенные адреса потом прогоняются через bulk-валидацию.
Массовая проверка: очередь + batch API
Не каждый адрес нужно проверять в реальном времени. Для очистки существующей базы или отложенной валидации подходит пакетный подход: собираем адреса в очередь, отправляем batch-запрос к API.
import fs from 'node:fs/promises';
const API_BASE = 'https://api.uchecker.net/api/v1';
const headers = {
'Content-Type': 'application/json',
'x-api-key': process.env.UCHECKER_API_KEY,
};
// 1. Загружаем файл со списком email
async function uploadList(filePath) {
const emails = (await fs.readFile(filePath, 'utf-8'))
.split('\n')
.map(e => e.trim())
.filter(Boolean);
const res = await fetch(`${API_BASE}/validate/bulk`, {
method: 'POST',
headers,
body: JSON.stringify({ emails }),
});
const { task_id } = await res.json();
return task_id;
}
// 2. Проверяем статус задачи (polling)
async function waitForResult(taskId, interval = 5000) {
while (true) {
const res = await fetch(`${API_BASE}/tasks/${taskId}`, { headers });
const task = await res.json();
if (task.status === 'completed') return task;
if (task.status === 'failed') throw new Error('Task failed');
await new Promise(r => setTimeout(r, interval));
}
}
// 3. Запуск
const taskId = await uploadList('./emails.txt');
console.log('Task created:', taskId);
const result = await waitForResult(taskId);
console.log('Deliverable:', result.summary.deliverable);
console.log('Undeliverable:', result.summary.undeliverable);
console.log('Risky:', result.summary.risky);Для списков больше 10 000 адресов добавьте разбиение на batch-и по 5-10 тысяч и обработку rate limit (HTTP 429). Подробнее об оптимизации bulk-запросов - в отдельной статье про API.
Best practices для продакшена
Техническая интеграция - половина дела. Вторая половина - правильная архитектура вокруг валидации.
Нормализуйте перед проверкой. Приведите email к lowercase. Для Gmail удалите точки в локальной части и всё после +. Один и тот же человек с адресами John.Doe+promo@Gmail.Com и johndoe@gmail.com не должен попадать в базу дважды.
function normalizeEmail(email) {
let [local, domain] = email.trim().toLowerCase().split('@');
if (['gmail.com', 'googlemail.com'].includes(domain)) {
local = local.replace(/\./g, '').split('+')[0];
domain = 'gmail.com';
}
return `${local}@${domain}`;
}Кешируйте результаты. Если пользователь исправляет адрес и снова отправляет форму, не нужно тратить ещё один API-запрос на тот же адрес. Простой in-memory кеш с TTL в 10 минут достаточен для формы регистрации.
const cache = new Map();
const TTL = 10 * 60 * 1000; // 10 минут
async function validateWithCache(email) {
const key = normalizeEmail(email);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < TTL) {
return cached.result;
}
const result = await validateEmail(key);
cache.set(key, { result, timestamp: Date.now() });
// Ограничиваем размер кеша
if (cache.size > 1000) {
const oldest = cache.keys().next().value;
cache.delete(oldest);
}
return result;
}Не блокируйте регистрацию при сбое API. Graceful degradation важнее точности. Если сервис валидации временно недоступен, принимайте адрес и ставьте в очередь на отложенную проверку. Потерять пользователя хуже, чем пропустить один сомнительный email.
Храните результат валидации. Добавьте в таблицу пользователей поля email_status и email_checked_at. При рассылке фильтруйте по статусу. Через 3-6 месяцев перепроверяйте: ящики закрываются, домены истекают.
Rate limit на эндпоинте. Ваш API-эндпоинт для валидации доступен извне. Бот может отправить тысячи запросов и выбрать ваш баланс. Ограничьте 5-10 запросов в минуту с одного IP.
API-ключ только на сервере. Никогда не отправляйте ключ в клиентский JavaScript. Все запросы к сервису валидации идут через ваш бэкенд. Переменная окружения UCHECKER_API_KEY не должна попадать в бандл.
Сравнение подходов
Выбор зависит от контекста. Вот сводка, которая поможет принять решение.
| Подход | Что проверяет | Скорость | Ограничения |
|---|---|---|---|
| email-validator | Синтаксис | < 1 мс | Не проверяет существование ящика |
| Zod / Joi / Yup | Синтаксис + схема | < 1 мс | Аналогично, только формат |
| dns.resolveMx | MX-записи домена | 50-200 мс | Не проверяет конкретный ящик |
| deep-email-validator | Синтаксис + MX + SMTP | 1-5 сек | SMTP блокируется облачными IP |
| Внешний API | Все уровни + AI-скоринг | 1-3 сек | Платный, зависимость от сервиса |
На практике подходы комбинируются. Синтаксическая проверка на каждом запросе, API-валидация при регистрации, периодическая bulk-проверка всей базы. Каждый уровень закрывает свой тип проблем.
Что выбрать для вашего проекта
Пет-проект или MVP. email-validator или z.string().email(). Быстро, бесплатно, закрывает базовые опечатки. На ранней стадии и этого достаточно.
Продукт с формой регистрации. Синтаксическая проверка + API-валидация на бэкенде. Один вызов на регистрацию стоит доли цента, но экономит на ESP-счетах и поддержании репутации.
Рассылочный сервис или CRM. Полный конвейер: синтаксис + API на входе, регулярная bulk-валидация базы, хранение статусов и автоматическое исключение невалидных из рассылок.
Общее правило: чем дороже каждый контакт в базе, тем больше уровней валидации оправдано. Для новостной рассылки с бесплатным контентом regex может быть достаточным. Для SaaS, где каждый пользователь - потенциальный клиент на тысячи долларов, API-проверка окупается с первого дня.
Синтаксическая проверка ловит 60% мусора бесплатно. API закрывает оставшиеся 40%, включая catch-all, disposable и спам-ловушки. Выбор не между одним или другим, а в том, сколько уровней нужно вашему проекту.
Попробуйте API валидации на реальных адресах: зарегистрируйтесь в uChecker и получите 30 бесплатных проверок для тестирования интеграции.
