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.