Para validar um endereço de e-mail no Express, você monta um middleware, não uma
única checagem — e esse middleware precisa responder a três perguntas diferentes,
em ordem. O endereço tem o formato correto? O domínio dele consegue receber
e-mail? A caixa postal realmente existe? O Express em si não traz nenhuma dessas
respostas, mas o ecossistema em torno dele reduz cada uma a poucas linhas:
express-validator para a sintaxe, o dns/promises embutido no Node para o
domínio e uma API de verificação para a caixa postal. Este guia constrói as três
como validadores em uma única rota POST e mostra exatamente onde o framework
para e onde uma checagem de verdade começa. É a versão em formato Express do
guia de validação de e-mail em Node.js,
então todo o raciocínio sobre DNS e SMTP de lá continua valendo por inteiro.
A resposta curta#
Encadeie três validadores no campo email, do mais barato ao mais caro, e deixe
o .bail() do express-validator interromper o fluxo assim que um deles for
decisivo: use body('email').isEmail() para a sintaxe, um validador personalizado
assíncrono envolvendo o resolveMx do dns/promises para o domínio e um segundo
validador personalizado assíncrono que chama uma API de verificação para a caixa
postal — retornando um 422 quando o veredito é undeliverable. Não tente abrir
conexões SMTP a partir do seu processo Express para sondar caixas postais por
conta própria: a porta 25 de saída é bloqueada na maioria dos hosts, e a resposta
depende da reputação do IP de envio e do greylisting que você não vai querer
reimplementar. Cada camada elimina endereços de forma mais barata que a anterior;
só a API consegue confirmar um endereço como válido.
Camada 1: sintaxe com express-validator#
O express-validator é o middleware de validação de fato para o Express — um
invólucro fino e nativo do Express em torno da biblioteca
validator.js. A checagem isEmail() dele é o isEmail do validator.js,
então você ganha o mesmo parser de sintaxe testado em batalha em que todo projeto
Node já se apoia, exposto como um middleware encadeável. Declare uma regra no
campo e ela roda antes do seu handler:
import { body } from "express-validator";
const validateEmail = body("email")
.trim()
.isEmail()
.withMessage("Enter a valid email address.")
.bail();
O .trim() sanitiza os espaços em branco no início e no fim, o .isEmail()
valida o formato, o .withMessage() define o texto do erro e o .bail() para a
cadeia desse campo no instante em que a sintaxe falha — de modo que as camadas
mais caras abaixo nunca rodem sobre uma string malformada. Esse .bail() é o
equivalente, no Express, a parar na checagem mais barata possível.
Saiba exatamente o que isso te dá. O isEmail() lê a string — ele nunca resolve
DNS e nunca abre um socket. definitely-fake@gmail.com passa.
info@company-that-folded.com passa. typo@gmial.com passa. Os três são
não entregáveis, e nenhum validador que apenas inspeciona a string jamais vai te
avisar disso — pela mesma razão que a
validação de e-mail por regex falha.
Sintaxe e entregabilidade são perguntas diferentes: uma é um fato sobre a string,
a outra é um fato sobre a internet.
Camada 2: o domínio consegue receber e-mail?#
Essa é a primeira camada que o Express não te entrega de bandeja, e é barata de
acoplar. Um domínio sem registros MX não consegue aceitar e-mail para ninguém,
então uma única consulta de DNS elimina domínios mortos, nomes de empresa
escritos errado e TLDs inventados antes que você gaste qualquer coisa na rede. O
node:dns/promises, embutido no Node, resolve registros MX sem nenhuma
dependência, e o .custom() do express-validator aceita uma função assíncrona
— lance um erro dentro dela para reprovar o campo, retorne true para aprovar:
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 isso é uma simples função assíncrona, ela encaixa direto na cadeia como um
validador personalizado, protegida por outro .bail() para que a camada de caixa
postal só rode em um domínio que de fato resolve uma rota de e-mail:
body("email")
.isEmail()
.bail()
.custom(hasMailRoute)
.bail();
await hasMailRoute("jane@gmail.com"); // true
await hasMailRoute("jane@company-that-folded.com"); // throws — no MX
Alguns domínios aceitam e-mail em um registro A sem MX (MX implícito). Se você
quiser respeitar esse caso extremo, recorra ao resolve4 quando o resolveMx
voltar vazio — mas, para a esmagadora maioria dos endereços reais, uma checagem de
MX é o filtro certo.
Camada 3: a caixa postal realmente existe?#
As camadas 1 e 2 só conseguem eliminar um endereço. Um domínio pode publicar
registros MX perfeitos e ainda assim não ter caixa postal no endereço que você tem
em mãos — noreply-9f2x@gmail.com é sintaxe válida em uma rota de e-mail ativa, e
ainda assim é uma caixa postal que nunca foi criada. Confirmar uma caixa postal
específica significa a conversa de entrega SMTP: conectar-se ao host de e-mail,
emitir RCPT TO, ler a resposta e desconectar antes de enviar qualquer coisa.
Em princípio, você poderia programar isso a partir do Express com o módulo net.
Na prática, você não deveria rodar isso a partir do servidor da sua aplicação: a
maioria dos hosts em nuvem bloqueia a porta 25 de saída, a resposta depende da
reputação do IP a partir do qual você se conecta, e os servidores receptores
aplicam greylisting e limitam a taxa de remetentes desconhecidos — de modo que uma
sondagem que funciona em um teste local silenciosamente falha, ou te coloca em
listas de bloqueio, em produção. O
funcionamento da verificação de e-mail
percorre toda a pipeline, incluindo domínios catch-all. Essa é a camada que vale
a pena delegar.
Chamando o Qualisend a partir de um validador personalizado assíncrono#
A delegação é apenas mais um validador personalizado. O endpoint de verificação do
Qualisend roda a pipeline inteira (sintaxe, DNS e a sondagem SMTP da caixa postal)
a partir de uma infraestrutura com reputação gerenciada. Ele responde na hora,
muitas vezes antes de a sondagem da caixa postal terminar, e o veredito da sondagem
chega depois a um job que você pode consultar mais tarde. Chame-o com o fetch
global (embutido no Node 18+), leia sua chave a partir de uma variável de ambiente,
limite a espera com um timeout e lance um erro apenas quando o veredito for
undeliverable:
async function isDeliverable(email) {
let res;
try {
res = await fetch("https://app.qualisend.com/api/v1/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(5000),
});
} catch {
// A timeout or a network error: don't block a real signup.
return true;
}
// Out of credits, rate limited or our own outage: don't block a real signup.
if (!res.ok) return true;
const { job_id, result } = await res.json();
if (result.status === "undeliverable") {
throw new Error("We couldn't confirm a mailbox at this address.");
}
// The mailbox probe is still running: collect its verdict off the request path.
if (result.smtp === "pending") {
recheckLater(email, job_id).catch(() => {});
}
return true;
}
A referência da API documenta a requisição e a resposta
completas. O veredito volta em um envelope { "result": { ... } }, ao lado do
job_id:
{
"job_id": "8c2f4e1a-5b7d-4c3e-9a6f-2d1b0e7c4a91",
"result": {
"status": "unknown",
"score": 60,
"smtp": "pending",
"reason": null,
"sub_flags": { "role": false, "disposable": false, "free_mail": false, "catch_all": false }
}
}
O status é um entre deliverable, risky, undeliverable ou unknown. O
validador acima reprova de forma definitiva apenas o undeliverable e deixa
passar risky e unknown, para que você decida mais adiante o que fazer com eles
— barrar com base no score ou ramificar por uma entrada de sub_flags como
disposable — em vez de transformar um endereço limítrofe em um erro de
formulário. Trate o JSON acima como o formato, não como o contrato; a lista
completa de campos está na documentação para desenvolvedores.
Quando smtp vale "pending", como no exemplo acima, a sondagem da caixa postal
ainda está rodando: reason continua null e o veredito final chega ao job. A
chamada a recheckLater busca esse veredito fora da requisição, consultando
GET /jobs/{job_id} a cada poucos segundos até o job terminar. Em produção,
passe o id do job para uma fila ou um worker em segundo plano em vez de consultar
dentro do próprio processo:
async function recheckLater(email, jobId) {
const url = `https://app.qualisend.com/api/v1/jobs/${jobId}?include=results`;
const headers = { Authorization: `Bearer ${process.env.QUALISEND_API_KEY}` };
for (let attempt = 0; attempt < 180; attempt++) {
await new Promise((resolve) => setTimeout(resolve, 5000));
let job;
try {
const res = await fetch(url, { headers, signal: AbortSignal.timeout(5000) });
if (!res.ok) continue;
job = await res.json();
} catch {
continue; // a network blip: try again on the next tick
}
if (job.status === "canceled" || job.status === "failed") return;
if (job.status === "completed") {
const [final] = job.results ?? [];
if (final?.status === "undeliverable") {
await flagEmail(email); // your own follow-up, e.g. ask the user to fix it
}
return;
}
}
}
Juntando as camadas para validar um endereço de e-mail no Express#
Componha as três em uma única cadeia body("email"), do mais barato ao mais caro,
com .bail() entre cada uma para que uma falha interrompa o fluxo antes que a
próxima camada rode. O validador da API só dispara em um endereço que já passou
pela sintaxe e pela checagem de MX:
import { body } from "express-validator";
const validateEmail = [
body("email")
.trim()
.isEmail()
.withMessage("Enter a valid email address.")
.bail()
.custom(hasMailRoute)
.bail()
.custom(isDeliverable),
];
Coloque esse array na frente do seu handler de rota e depois leia os erros
coletados com o validationResult. Quando a cadeia falha, responda com um 422 e
o array de erros; caso contrário, todas as camadas passaram e o endereço está
seguro 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 });
});
Essa ordem é o truque inteiro: o isEmail() corta o lixo antes que seu código
rode, a checagem local de MX descarta domínios mortos de graça, e a API é acionada
apenas para endereços que passaram pelas duas. Uma validação na ordem errada — ou
que pula as camadas baratas — gasta um crédito de API a cada erro de digitação. É
a estratificação, não o framework, que torna a validação confiável; é o mesmo
formato da versão em Node.js, com a cadeia
de middleware do Express fazendo o papel da pipeline.
Uma decisão a tomar logo de cara: o que acontece quando a própria chamada da API
falha. Um timeout de rede ou um fetch que lança um erro dentro do
isDeliverable não deveria entregar a um cliente real um 422 que ele não
consegue corrigir. O validador acima já suaviza esse caso: ele retorna true diante
de um timeout, de um erro de rede ou de uma resposta não OK, como um 402, um
429 ou um 5xx, tratando um serviço inacessível como unknown em vez de
undeliverable, deixando o cadastro passar e adiando a reverificação para depois.
As camadas de sintaxe e de MX já rodaram localmente, então você só está relaxando
a camada que depende da rede.
Rode essa pipeline síncrona e rápida no ponto de coleta e reserve a verificação em lote, mais pesada, para a limpeza de listas. O guia de cadastro serverless mostra o padrão em tempo real de ponta a ponta, e, na hora de escolher qual serviço fica por trás da camada três, a comparação de APIs coloca as opções lado a lado.
Perguntas frequentes
01O isEmail do express-validator é suficiente para validar um endereço de e-mail?
Para a sintaxe, sim - body("email").isEmail() encapsula o isEmail do validator.js e é a checagem certa para a primeira camada, muito melhor do que um regex feito à mão. Mas ele valida o formato, não a entregabilidade: nunca resolve DNS nem contata um servidor de e-mail, então uma aprovação significa "parece um e-mail", não "vai ser entregue". Combine-o com uma consulta de MX e uma checagem de caixa postal antes de confiar no endereço.
02Como escrevo um validador personalizado assíncrono no Express?
Passe uma função assíncrona para o .custom() do express-validator. A função recebe o valor do campo; retorne true (ou resolva) para aprovar e throw um Error (ou rejeite) para reprovar - a mensagem lançada vira o erro de validação. É assim que as funções hasMailRoute e isDeliverable acima se encaixam na mesma cadeia body("email"). Coloque um .bail() antes de cada uma para que uma camada reprovada interrompa a cadeia em vez de executar a próxima.
03Dá para verificar se uma caixa postal existe no Express sem uma API?
Em parte. O node:dns/promises confirma que o domínio aceita e-mail, o que descarta domínios mortos de graça e não exige nenhuma dependência. Confirmar a caixa postal exige uma conversa SMTP que você pode tentar com o módulo net, mas não deveria rodar a partir do servidor da sua aplicação - a porta 25 é amplamente bloqueada e o resultado depende da reputação do seu IP. Essa é a camada que um serviço de verificação existe para resolver; o que é a verificação de e-mail cobre os termos.
04Devo retornar um 422 ou deixar o cadastro passar quando o resultado é desconhecido?
Rejeite undeliverable com um 422 - é um endereço comprovadamente ruim que o usuário pode corrigir. Mas não reprove por causa de uma falha do seu próprio serviço de verificação: trate uma resposta não OK, um timeout ou um status unknown como "deixa passar e revalida depois", para que um problema de rede passageiro nunca bloqueie um cadastro real. O validador isDeliverable faz exatamente isso ao retornar true quando a resposta não é OK.
Pronto para adicionar a camada de caixa postal? Jogue um endereço sintaticamente
perfeito no verificador de e-mail gratuito para ver uma
string aprovada pelo isEmail voltar como undeliverable, e depois conecte o
mesmo veredito ao seu middleware Express com os exemplos de copiar e colar da
referência da API.



