uCheckeruChecker
12 мин чтения

Валидация 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-validator
import * 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-validator
import { 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.resolveMxMX-записи домена50-200 мсНе проверяет конкретный ящик
deep-email-validatorСинтаксис + MX + SMTP1-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 бесплатных проверок для тестирования интеграции.

Node.js валидация emailemail validation npmпроверка email JavaScriptemail-validator npmNode.js SMTP проверкаdeep-email-validatorZod email validationExpress middleware