Skip to content
Empieza con 100 créditos de verificación gratis
Qualisend
Todos los artículos
Ingeniería / 6 de junio de 2026

Cómo validar una dirección de correo en Node.js

5 minutes read

Qualisend team
Una ventana de código Node.js validando un correo mediante las capas de sintaxis, DNS y SMTP

Validar una dirección de correo en Node.js son en realidad tres tareas bajo un mismo nombre. La mayoría de los tutoriales te enseñan una expresión regular y se detienen ahí, comprobando la ortografía y llamándolo validación. La validación de verdad es por capas: una comprobación de sintaxis barata, una consulta DNS y un sondeo SMTP del buzón, y Node te da las dos primeras de serie. Esta guía construye cada capa con código funcional y muestra dónde toma el relevo una API de verificación.

La respuesta rápida#

Usa una expresión regular permisiva para la sintaxis, node:dns/promises para la consulta de MX y una API de verificación para la comprobación SMTP del buzón; en ese orden, primero lo más barato, cortocircuitando en cuanto una capa sea decisiva. No intentes abrir conexiones SMTP desde el servidor de tu aplicación: el puerto 25 está bloqueado en la mayoría de los hosts y, aun donde no lo está, la comprobación depende de la reputación de la IP de envío y del manejo del greylisting que no querrás tener que construir.

Capa 1: sintaxis#

La expresión regular pertenece aquí y en ningún otro sitio. Mantenla permisiva: el objetivo es cazar erratas de dedo en la entrada, no reimplementar el RFC 5322 (que de todos modos no ayuda):

const SYNTAX = /^[^\s@"]+(?:\.[^\s@"]+)*@[^\s@.]+(?:\.[^\s@.]+)+$/;

export function isValidSyntax(email) {
  return typeof email === "string" && email.length <= 320 && SYNTAX.test(email);
}

Pasar esta capa significa "vale la pena comprobarlo", no "válido". Todas las capas siguientes dan por hecho que la sintaxis ya es razonable.

Capa 2: ¿puede el dominio recibir correo?#

Aquí es donde Node se gana el sueldo. El módulo integrado node:dns/promises resuelve registros MX sin dependencias, y un dominio sin ruta de correo no puede aceptar correo para nadie, así que esta única consulta elimina dominios inactivos, nombres de empresa mal escritos y TLD inventados:

import { resolveMx } from "node:dns/promises";

export async function hasMailRoute(domain) {
  try {
    const mx = await resolveMx(domain);
    return mx.length > 0;
  } catch {
    // ENOTFOUND / ENODATA → domain doesn't exist or publishes no MX
    return false;
  }
}
await hasMailRoute("gmail.com");            // true
await hasMailRoute("company-that-folded.com"); // false

Algunos dominios aceptan correo en un registro A sin MX (MX implícito). Si quieres contemplar ese caso límite, recurre a dns.resolve4 cuando resolveMx esté vacío; pero para la inmensa mayoría de las direcciones reales, una comprobación de MX es el filtro adecuado.

Capa 3: ¿existe realmente el buzón?#

Las capas 1 y 2 solo pueden descartar una dirección. Confirmar un buzón implica la conversación de entrega SMTP: RCPT TO, leer la respuesta y desconectar antes de enviar nada. En principio puedes hacerlo con el módulo net. En la práctica no deberías: la mayoría de los proveedores de nube bloquean el puerto 25 saliente, la respuesta depende de la reputación de la IP desde la que te conectas, y los servidores aplican greylisting y limitan el ritmo a remitentes desconocidos. Esta es la capa que vale la pena delegar.

El POST /verify de Qualisend ejecuta el pipeline completo y devuelve un veredicto. Las comprobaciones locales vuelven de inmediato con el sondeo SMTP en cola:

const BASE = "https://app.qualisend.com/api/v1";

export async function verify(email) {
  const res = await fetch(`${BASE}/verify`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email }),
  });
  if (!res.ok) throw new Error(`Qualisend responded ${res.status}`);
  return res.json();
}
{
  "job_id": "6f1c2e0a-9b3d-4a1e-8c77-1a2b3c4d5e6f",
  "probe_queued": true,
  "result": {
    "email": "jane@example.com",
    "status": "deliverable",
    "score": 95,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false },
    "did_you_mean": null,
    "smtp": "pending",
    "reason": null
  }
}

Para la validación de registro en vivo, el result inmediato suele bastar para actuar: rechaza undeliverable, ofrece la corrección did_you_mean, marca disposable. Cuando necesites el veredicto confirmado por SMTP, sondea el trabajo hasta que termine la comprobación:

export async function verifyAndWait(email, { tries = 10, delayMs = 1500 } = {}) {
  const { job_id } = await verify(email);
  for (let i = 0; i < tries; i++) {
    const res = await fetch(`${BASE}/jobs/${job_id}?include=results`, {
      headers: { Authorization: `Bearer ${process.env.QUALISEND_API_KEY}` },
    });
    const job = await res.json();
    if (job.status === "completed") return job.results[0]; // { status, score, reason, ... }
    await new Promise((r) => setTimeout(r, delayMs));
  }
  return null; // still processing — treat as unknown, retry later
}

Hay dos cosas que hacen tropezar al fetch ingenuo de arriba en cuanto se topa con tráfico real. Primero, un servicio remoto lento no debería colgar tu proceso de Node: envuelve la petición en un timeout con AbortController para que falle rápido en lugar de bloquear el bucle de eventos. Segundo, un 429 (límite de ritmo) o un 5xx es transitorio y merece un reintento, mientras que un 4xx como 422 significa que el payload está mal y reintentar no servirá de nada, así que separa ambos casos:

export async function verify(email, { timeoutMs = 4000 } = {}) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
  try {
    const res = await fetch(`${BASE}/verify`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: ctrl.signal,
    });
    if (res.status === 429 || res.status >= 500) {
      throw Object.assign(new Error(`retryable ${res.status}`), { retryable: true });
    }
    if (!res.ok) throw new Error(`Qualisend responded ${res.status}`);
    return res.json();
  } finally {
    clearTimeout(timer);
  }
}

Guarda la clave de API en process.env, nunca en el bundle: esta llamada pertenece al lado servidor de tu aplicación Node, no a nada que envíes al navegador.

Para la limpieza de listas estás verificando miles de direcciones, no una a una. No hagas un bucle con await (serial y lento) ni las dispares todas a la vez (harás saltar el límite de ritmo). Envía un número acotado de peticiones concurrentes con Promise.allSettled para que una dirección rechazada no hunda todo el lote:

export async function verifyMany(emails, { concurrency = 5 } = {}) {
  const out = [];
  for (let i = 0; i < emails.length; i += concurrency) {
    const slice = emails.slice(i, i + concurrency);
    const settled = await Promise.allSettled(slice.map((e) => verify(e)));
    out.push(...settled); // each: { status: "fulfilled", value } | { status: "rejected", reason }
  }
  return out;
}

Recoge las entradas rejected, aplica un backoff y reintenta solo esas: los veredictos fulfilled ya están hechos. Este mismo patrón regulado te mantiene por debajo del límite de ritmo del plan sin ninguna dependencia adicional.

Uniendo las capas#

Primero lo más barato, detente en cuanto tengas una respuesta:

export async function validateEmail(email) {
  if (!isValidSyntax(email)) return { status: "undeliverable", reason: "invalid_email" };
  const domain = email.slice(email.lastIndexOf("@") + 1);
  if (!(await hasMailRoute(domain))) return { status: "undeliverable", reason: "invalid_domain" };
  const { result } = await verify(email);
  return result; // deliverable | risky | undeliverable | unknown, with reason + sub_flags
}

Las dos capas locales no cuestan nada y cazan la mayor parte de la basura al instante; la capa de API se ejecuta solo sobre las direcciones que merecen la ida y vuelta por la red. Ese orden es todo el truco: consulta cómo funciona la verificación de correos para entender por qué cada etapa está donde está.

Preguntas frecuentes#

¿Basta con una biblioteca como validator.js o email-validator?#

Esas bibliotecas resuelven bien la capa 1 (la sintaxis) y son un buen sustituto de una expresión regular hecha a mano. Pero son comprobadores de sintaxis: no resuelven registros MX ni confirman un buzón, así que un validator.isEmail() correcto sigue significando "parece un correo", no "se entregará". Combínalas con las capas de DNS y SMTP de arriba.

¿Puedo comprobar si un correo existe desde Node sin una API?#

En parte. node:dns/promises confirma que el dominio acepta correo, lo que descarta gratis los dominios inactivos. Confirmar el buzón implica un sondeo SMTP, que puedes intentar con el módulo net pero no deberías ejecutar desde el servidor de tu aplicación: el puerto 25 está bloqueado en muchos sitios y el resultado depende de la reputación de tu IP. Esa es la capa que existe para que la gestione un servicio de verificación.

¿Debería validar los correos de forma síncrona en el registro?#

Haz las capas instantáneas de forma síncrona: la sintaxis y los MX son lo bastante rápidos como para bloquear la petición y dar feedback inmediato al usuario. Trata el resultado SMTP como la respuesta más lenta: actúa según el veredicto local inmediato en el registro y usa el resultado completo confirmado por SMTP para la limpieza de listas. La guía de registro serverless muestra el patrón de principio a fin.

¿Debería cachear los resultados de verificación para no volver a comprobar la misma dirección?#

Sí. Cada llamada de verificación es una ida y vuelta por la red y un crédito gastado, así que no la repitas para una dirección que acabas de comprobar. Indexa una caché de vida corta por el correo normalizado (primero pásalo a minúsculas y recorta los espacios) y reutiliza el veredicto durante unos minutos. Una advertencia: no caches los resultados unknown ni los que sigan pending, porque son justo los que conviene reintentar cuando termine el sondeo SMTP. Un simple Map basta para un único proceso de Node; recurre a Redis solo cuando ejecutes más de una instancia y necesites que compartan la caché.


¿Listo para añadir la capa SMTP? El plan gratuito incluye 100 créditos que ejecutan el pipeline completo, y la referencia de la API tiene los endpoints completos /verify y /jobs con ejemplos para copiar y pegar en siete lenguajes.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started