Skip to content
Comece com 100 créditos de verificação grátis
Qualisend
Todos os artigos
Engenharia / 11 de maio de 2026

Como validar um endereço de e-mail no Express

9 min de leitura

Qualisend team
Como validar um endereço de e-mail no Express

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.

Sua reputação, protegida.

Limpe sua primeira lista em minutos. 100 créditos grátis, sem cartão.

Começar