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

Cómo validar una dirección de correo en Express

9 minutes read

Qualisend team
Una ventana de código Express filtrando un correo a través de las capas de validación de sintaxis, MX y buzón

Para validar una dirección de correo en Express montas middleware, no una única comprobación, y ese middleware tiene que responder a tres preguntas distintas en orden. ¿Tiene la dirección la forma correcta? ¿Puede su dominio recibir correo? ¿Existe realmente el buzón? Express no incluye ninguna de estas comprobaciones, pero el ecosistema que lo rodea reduce cada una a unas pocas líneas: express-validator para la sintaxis, el dns/promises integrado de Node para el dominio y una API de verificación para el buzón. Esta guía construye las tres como validadores sobre una única ruta POST y muestra exactamente dónde termina el framework y dónde empieza una comprobación de verdad. Es la versión con forma de Express de la guía de validación de correo en Node.js, así que todo el razonamiento sobre DNS y SMTP de allí sigue siendo válido aquí.

La respuesta rápida#

Encadena tres validadores en el campo email, del más barato al más caro, y deja que el .bail() de express-validator corte en corto en cuanto uno sea decisivo: usa body('email').isEmail() para la sintaxis, un validador personalizado asíncrono que envuelva el resolveMx de dns/promises para el dominio, y un segundo validador personalizado asíncrono que llame a una API de verificación para el buzón, devolviendo un 422 cuando el veredicto sea undeliverable. No intentes abrir conexiones SMTP desde tu proceso de Express para sondear buzones por tu cuenta: el puerto 25 saliente está bloqueado en la mayoría de los hosts, y la respuesta depende de la reputación de la IP emisora y del greylisting que no querrás reimplementar. Cada capa descarta direcciones más barato que la anterior; solo la API puede darlas por válidas.

Capa 1: sintaxis con express-validator#

express-validator es el middleware de validación de facto para Express: un envoltorio fino y nativo de Express en torno a la librería validator.js. Su comprobación isEmail() es el isEmail de validator.js, así que obtienes el mismo analizador de sintaxis probado en combate en el que ya se apoya cualquier proyecto de Node, expuesto como un middleware encadenable. Declara una regla sobre el campo y se ejecuta antes que tu manejador:

import { body } from "express-validator";

const validateEmail = body("email")
  .trim()
  .isEmail()
  .withMessage("Enter a valid email address.")
  .bail();

.trim() sanea los espacios en blanco iniciales y finales, .isEmail() valida la forma, .withMessage() fija el texto del error y .bail() detiene la cadena para este campo en el momento en que falla la sintaxis, de modo que las capas más costosas de abajo nunca se ejecutan sobre una cadena mal formada. Ese .bail() es el equivalente en Express de detenerse en la comprobación más barata posible.

Ten claro qué te aporta esto exactamente. isEmail() lee la cadena: nunca resuelve DNS y nunca abre un socket. definitely-fake@gmail.com pasa. info@company-that-folded.com pasa. typo@gmial.com pasa. Las tres son inentregables, y ningún validador que solo inspeccione la cadena te lo dirá jamás, por la misma razón por la que la validación de correo con expresiones regulares falla. La sintaxis y la entregabilidad son preguntas distintas: una es un hecho sobre la cadena, la otra es un hecho sobre internet.

Capa 2: ¿puede el dominio recibir correo?#

Esta es la primera capa que Express no te da hecha, y es barata de añadir. Un dominio sin registros MX no puede aceptar correo para nadie, así que una consulta DNS elimina dominios muertos, nombres de empresa mal escritos y TLD inventados antes de gastar nada en la red. El node:dns/promises integrado de Node resuelve los registros MX sin dependencias, y el .custom() de express-validator acepta una función asíncrona: lanza dentro de ella para fallar el campo, devuelve true para aprobar:

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

async function hasMailRoute(email) {
  const domain = email.slice(email.lastIndexOf("@") + 1);

  let records = [];
  try {
    records = await resolveMx(domain);
  } catch {
    // ENOTFOUND / ENODATA — the domain doesn't exist or publishes no MX
    throw new Error("This domain cannot receive email.");
  }

  if (records.length === 0) {
    throw new Error("This domain cannot receive email.");
  }
  return true;
}

Como es una función asíncrona normal, encaja directamente en la cadena como un validador personalizado, protegido por otro .bail() para que la capa del buzón solo se ejecute sobre un dominio que realmente resuelva una ruta de correo:

body("email")
  .isEmail()
  .bail()
  .custom(hasMailRoute)
  .bail();
await hasMailRoute("jane@gmail.com");               // true
await hasMailRoute("jane@company-that-folded.com"); // throws — no MX

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

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

Las capas 1 y 2 solo pueden descartar una dirección. Un dominio puede publicar registros MX perfectos y aun así no tener ningún buzón en la dirección que manejas: noreply-9f2x@gmail.com es sintaxis válida sobre una ruta de correo activa, y sigue siendo un buzón que nunca se creó. Confirmar un buzón concreto implica la conversación de entrega SMTP: conectar con el servidor de correo, emitir RCPT TO, leer la respuesta y desconectar antes de enviar nada.

En principio podrías escribir ese guion desde Express con el módulo net. En la práctica no deberías ejecutarlo desde tu servidor de aplicación: la mayoría de los hosts en la nube bloquean el puerto 25 saliente, la respuesta depende de la reputación de la IP desde la que te conectas, y los servidores receptores aplican greylisting y limitan la tasa a remitentes desconocidos, de modo que un sondeo que funciona en una prueba local falla sin aviso, o te mete en una lista de bloqueo, en producción. Cómo funciona la verificación de correo recorre todo el proceso, dominios catch-all incluidos. Esta es la capa que vale la pena delegar.

Llamar a Qualisend desde un validador personalizado asíncrono#

La delegación no es más que otro validador personalizado. El endpoint de verificación de Qualisend ejecuta todo el proceso —sintaxis, DNS y el sondeo SMTP del buzón— desde una infraestructura con reputación gestionada y devuelve un veredicto. Llámalo con el fetch global (integrado en Node 18+), lee tu clave desde una variable de entorno y lanza solo cuando el veredicto sea undeliverable:

async function isDeliverable(email) {
  const res = await fetch("https://api.qualisend.com/v1/verify", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email }),
  });

  // On our own outage or a rate limit, don't block a real signup.
  if (!res.ok) return true;

  const { result } = await res.json();

  if (result.status === "undeliverable") {
    throw new Error("We couldn't confirm a mailbox at this address.");
  }
  return true;
}

El endpoint de arriba es un marcador de posición —consulta la referencia de la API para la URL base y la forma exacta de la petición—, pero la respuesta vuelve como un sobre { "result": { ... } }:

{
  "result": {
    "status": "deliverable",
    "score": 95,
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
  }
}

status es uno de deliverable, risky, undeliverable o unknown. El validador de arriba solo falla en firme con undeliverable y deja pasar risky y unknown, de modo que puedes decidir aguas abajo qué hacer con ellos —filtrar por score, o ramificar según una entrada de sub_flags como disposable— en lugar de convertir una dirección dudosa en un error de formulario. Trata el JSON de arriba como la forma, no como el contrato; la lista completa de campos está en la documentación para desarrolladores.

Uniendo las capas para validar una dirección de correo en Express#

Compón las tres en una sola cadena body("email"), del más barato al más caro, con un .bail() entre cada una para que un fallo corte en corto antes de que se ejecute la siguiente capa. El validador de la API solo se dispara sobre una dirección que ya superó la sintaxis y la comprobación MX:

import { body } from "express-validator";

const validateEmail = [
  body("email")
    .trim()
    .isEmail()
    .withMessage("Enter a valid email address.")
    .bail()
    .custom(hasMailRoute)
    .bail()
    .custom(isDeliverable),
];

Coloca ese array delante de tu manejador de ruta y luego lee los errores recogidos con validationResult. Cuando la cadena falla, responde con un 422 y el array de errores; en caso contrario, todas las capas han pasado y la dirección es segura para persistir:

import express from "express";
import { validationResult } from "express-validator";

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

app.post("/signup", validateEmail, (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }

  // Every layer passed — safe to create the account.
  const { email } = req.body;
  return res.status(201).json({ email });
});

Ese orden es todo el truco: isEmail() corta la basura antes de que se ejecute tu código, la comprobación MX local descarta dominios muertos por nada, y la API solo se invoca para direcciones que superaron ambas. Validar en el orden equivocado —o saltarse las capas baratas— gasta un crédito de API en cada errata. Lo que hace fiable la validación es la estratificación, no el framework; tiene la misma forma que la versión de Node.js, con la cadena de middleware de Express haciendo las veces del proceso.

Una decisión que hay que tomar de antemano: qué pasa cuando falla la propia llamada a la API. Un tiempo de espera de red o un fetch que lanza dentro de isDeliverable no debería entregarle a un cliente real un 422 que no puede corregir. El validador de arriba ya suaviza ese caso devolviendo true ante una respuesta no OK —tratando un servicio inaccesible como unknown en lugar de undeliverable, dejando pasar el registro y aplazando la re-verificación para más tarde—. Las capas de sintaxis y MX ya se ejecutaron localmente, así que solo estás relajando la capa que depende de la red.

Ejecuta este proceso síncrono y rápido en el punto de captación y reserva la verificación por lotes, más pesada, para la limpieza de listas. La guía de registro serverless muestra el patrón en tiempo real de principio a fin, y cuando estés eligiendo qué servicio respalda la tercera capa, la comparativa de APIs alinea las opciones.

Preguntas frecuentes#

¿Basta con el isEmail de express-validator para validar una dirección de correo?#

Para la sintaxis, sí: body("email").isEmail() envuelve el isEmail de validator.js y es la comprobación adecuada para la primera capa, muchísimo mejor que una expresión regular hecha a mano. Pero valida la forma, no la entregabilidad: nunca resuelve DNS ni contacta con un servidor de correo, así que un resultado válido significa «parece un correo», no «se entregará». Combínalo con una consulta MX y una comprobación del buzón antes de fiarte de la dirección.

¿Cómo escribo un validador personalizado asíncrono en Express?#

Pasa una función asíncrona al .custom() de express-validator. La función recibe el valor del campo; devuelve true (o resuelve) para aprobar, y throw un Error (o rechaza) para fallar: el mensaje lanzado se convierte en el error de validación. Así es como las funciones hasMailRoute e isDeliverable de arriba se conectan a la misma cadena body("email"). Pon un .bail() antes de cada una para que una capa fallida detenga la cadena en lugar de ejecutar la siguiente.

¿Puedo comprobar si un buzón existe en Express sin una API?#

En parte. node:dns/promises confirma que el dominio acepta correo, lo que descarta dominios muertos gratis y sin necesidad de dependencias. Confirmar el buzón implica una conversación SMTP que puedes intentar con el módulo net, pero que no deberías ejecutar desde tu servidor de 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; qué es la verificación de correo explica los términos.

¿Debo devolver un 422 o dejar pasar el registro ante un resultado desconocido?#

Rechaza undeliverable con un 422: es una dirección incorrecta confirmada que el usuario puede corregir. Pero no falles por una caída de tu propio servicio de verificación: trata una respuesta no OK, un tiempo de espera agotado o un estado unknown como «déjalo pasar y vuelve a comprobarlo más tarde», de modo que un problema de red transitorio nunca bloquee un registro real. El validador isDeliverable hace exactamente esto devolviendo true cuando la respuesta no es OK.


¿Listo para añadir la capa del buzón? Deja caer una dirección sintácticamente perfecta en el comprobador de correo gratuito para ver cómo una cadena aprobada por isEmail vuelve como undeliverable, y luego conecta ese mismo veredicto a tu middleware de Express con los ejemplos para copiar y pegar de la referencia de la API.

Your reputation, protected.

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

Get started