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 e devolve um veredito.
Chame-o com o fetch global (embutido no Node 18+), leia sua chave a partir de
uma variável de ambiente e lance um erro apenas quando o veredito for
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;
}
O endpoint acima é um espaço reservado — consulte a
referência da API para a URL base exata e o formato da requisição —
mas a resposta volta como um envelope { "result": { ... } }:
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "free": 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.
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 ao retornar true diante
de uma resposta não OK — 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#
O 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.
Como 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.
Dá 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.
Devo 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.