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

Como validar um endereço de e-mail no Node.js

5 minutes read

Qualisend team
Uma janela de código Node.js validando um e-mail pelas camadas de sintaxe, DNS e SMTP

Validar um endereço de e-mail no Node.js são três tarefas com um nome só. A maioria dos tutoriais mostra uma regex e para por aí, o que verifica a grafia e chama isso de validação. A validação de verdade é feita em camadas: uma verificação de sintaxe barata, uma consulta de DNS e um teste SMTP da caixa postal — e o Node já entrega as duas primeiras prontas. Este guia constrói cada camada com código funcional e mostra onde uma API de verificação assume o controle.

A resposta curta#

Use uma regex permissiva para a sintaxe, node:dns/promises para a consulta de MX e uma API de verificação para a checagem SMTP da caixa postal — nessa ordem, da mais barata para a mais cara, encerrando assim que uma delas for decisiva. Não tente abrir conexões SMTP a partir do servidor da sua aplicação: a porta 25 é bloqueada na maioria dos hosts e, mesmo onde não é, a checagem depende da reputação do IP de envio e do tratamento de greylisting que você não vai querer construir.

Camada 1: sintaxe#

A regex pertence a este lugar e a nenhum outro. Mantenha-a permissiva — o objetivo é pegar erros de digitação na entrada, não reimplementar a RFC 5322 (o que não ajuda mesmo):

const SYNTAX = /^[^\s@"]+(?:\.[^\s@"]+)*@[^\s@.]+(?:\.[^\s@.]+)+$/;

export function isValidSyntax(email) {
  return typeof email === "string" && email.length <= 320 && SYNTAX.test(email);
}

Passar aqui significa "vale a pena checar", não "válido". Toda camada seguinte pressupõe que a sintaxe já está sã.

Camada 2: o domínio consegue receber e-mail?#

É aqui que o Node justifica sua existência. O módulo nativo node:dns/promises resolve registros MX sem nenhuma dependência, e um domínio sem rota de e-mail não pode aceitar mensagens para ninguém — então essa única consulta elimina domínios mortos, nomes de empresa escritos errado e TLDs inventados:

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

export async function hasMailRoute(domain) {
  try {
    const mx = await resolveMx(domain);
    return mx.length > 0;
  } catch {
    // ENOTFOUND / ENODATA → domain doesn't exist or publishes no MX
    return false;
  }
}
await hasMailRoute("gmail.com");            // true
await hasMailRoute("company-that-folded.com"); // false

Alguns domínios aceitam e-mail em um registro A sem MX (MX implícito). Se você quiser respeitar esse caso extremo, faça um fallback para dns.resolve4 quando o resolveMx vier vazio — mas, para a esmagadora maioria dos endereços reais, uma checagem de MX é o filtro certo.

Camada 3: a caixa postal existe de verdade?#

As camadas 1 e 2 só conseguem descartar um endereço. Confirmar uma caixa postal significa a conversa de entrega SMTP — RCPT TO, ler a resposta, desconectar antes de enviar qualquer coisa. Em princípio você pode fazer isso com o módulo net. Na prática, não deveria: a maioria dos provedores de nuvem bloqueia a porta 25 de saída, a resposta depende da reputação do IP de onde você conecta, e os servidores aplicam greylisting e limitam a taxa de remetentes desconhecidos. Essa é a camada que vale a pena delegar.

O POST /verify da Qualisend roda o pipeline completo e devolve um veredito. As checagens locais voltam imediatamente com o teste SMTP na fila:

const BASE = "https://app.qualisend.com/api/v1";

export async function verify(email) {
  const res = await fetch(`${BASE}/verify`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email }),
  });
  if (!res.ok) throw new Error(`Qualisend responded ${res.status}`);
  return res.json();
}
{
  "job_id": "6f1c2e0a-9b3d-4a1e-8c77-1a2b3c4d5e6f",
  "probe_queued": true,
  "result": {
    "email": "jane@example.com",
    "status": "deliverable",
    "score": 95,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false },
    "did_you_mean": null,
    "smtp": "pending",
    "reason": null
  }
}

Para validação de cadastro ao vivo, o result imediato geralmente já basta para agir — rejeite undeliverable, ofereça a correção do did_you_mean, sinalize disposable. Quando você precisar do veredito confirmado por SMTP, faça polling do job até o teste terminar:

export async function verifyAndWait(email, { tries = 10, delayMs = 1500 } = {}) {
  const { job_id } = await verify(email);
  for (let i = 0; i < tries; i++) {
    const res = await fetch(`${BASE}/jobs/${job_id}?include=results`, {
      headers: { Authorization: `Bearer ${process.env.QUALISEND_API_KEY}` },
    });
    const job = await res.json();
    if (job.status === "completed") return job.results[0]; // { status, score, reason, ... }
    await new Promise((r) => setTimeout(r, delayMs));
  }
  return null; // still processing — treat as unknown, retry later
}

Duas coisas derrubam o fetch ingênuo acima assim que ele encontra tráfego real. Primeiro, um upstream lento não deveria travar o seu processo Node — envolva a requisição em um timeout com AbortController para que ela falhe rápido em vez de bloquear o event loop. Segundo, um 429 (limite de taxa atingido) ou um 5xx é transitório e vale a pena tentar de novo, ao passo que um 4xx como o 422 significa que o payload está errado e repetir não vai adiantar — então separe os dois:

export async function verify(email, { timeoutMs = 4000 } = {}) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
  try {
    const res = await fetch(`${BASE}/verify`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: ctrl.signal,
    });
    if (res.status === 429 || res.status >= 500) {
      throw Object.assign(new Error(`retryable ${res.status}`), { retryable: true });
    }
    if (!res.ok) throw new Error(`Qualisend responded ${res.status}`);
    return res.json();
  } finally {
    clearTimeout(timer);
  }
}

Mantenha a chave de API em process.env, nunca no bundle — essa chamada pertence ao lado servidor da sua aplicação Node, não a nada que você envie para o navegador.

Para a limpeza de listas, você está verificando milhares de endereços, não um de cada vez. Não faça um loop com await (serial e lento) nem dispare todos de uma vez (você vai estourar o limite de taxa). Envie um número limitado de requisições concorrentes com Promise.allSettled para que um endereço rejeitado não afunde o lote inteiro:

export async function verifyMany(emails, { concurrency = 5 } = {}) {
  const out = [];
  for (let i = 0; i < emails.length; i += concurrency) {
    const slice = emails.slice(i, i + concurrency);
    const settled = await Promise.allSettled(slice.map((e) => verify(e)));
    out.push(...settled); // each: { status: "fulfilled", value } | { status: "rejected", reason }
  }
  return out;
}

Junte as entradas rejected, dê um respiro e tente de novo só essas — os vereditos fulfilled já estão prontos. Esse mesmo padrão com estrangulamento mantém você abaixo do limite de taxa do plano sem nenhuma dependência extra.

Juntando as camadas#

Da mais barata para a mais cara, parando assim que você tiver uma resposta:

export async function validateEmail(email) {
  if (!isValidSyntax(email)) return { status: "undeliverable", reason: "invalid_email" };
  const domain = email.slice(email.lastIndexOf("@") + 1);
  if (!(await hasMailRoute(domain))) return { status: "undeliverable", reason: "invalid_domain" };
  const { result } = await verify(email);
  return result; // deliverable | risky | undeliverable | unknown, with reason + sub_flags
}

As duas camadas locais não custam nada e pegam a maior parte do lixo na hora; a camada da API roda apenas nos endereços que valem a ida e volta pela rede. Essa ordenação é o truque todo — veja como funciona a verificação de e-mail para entender por que cada etapa fica onde fica.

Perguntas frequentes#

Uma biblioteca como validator.js ou email-validator é suficiente?#

Essas bibliotecas fazem bem a camada 1 — sintaxe — e são uma ótima alternativa a uma regex feita na mão. Mas são verificadoras de sintaxe: elas não resolvem registros MX nem confirmam uma caixa postal, então um validator.isEmail() aprovado ainda significa "parece um e-mail", não "será entregue". Combine-as com as camadas de DNS e SMTP acima.

Consigo verificar se um e-mail existe no Node 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. Confirmar a caixa postal exige um teste 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 cuidar.

Devo validar e-mails de forma síncrona no cadastro?#

Faça as camadas instantâneas de forma síncrona — sintaxe e MX são rápidas o suficiente para bloquear a requisição e dar feedback imediato ao usuário. Trate o resultado do SMTP como a resposta mais lenta: aja com base no veredito local imediato no cadastro e use o resultado completo confirmado por SMTP para a limpeza de listas. O guia de cadastro serverless mostra o padrão de ponta a ponta.

Devo armazenar em cache os resultados de verificação para evitar checar o mesmo endereço de novo?#

Sim. Cada chamada de verificação é uma ida e volta pela rede e um crédito gasto, então não repita para um endereço que você acabou de checar. Use como chave o e-mail normalizado — deixe em minúsculas e remova os espaços primeiro — em um cache de vida curta e reaproveite o veredito por alguns minutos. Um detalhe: não guarde em cache resultados unknown ou ainda pending, porque são justamente os que vale a pena tentar de novo assim que o teste SMTP terminar. Um Map simples basta para um único processo Node; recorra ao Redis só quando você rodar mais de uma instância e precisar que elas compartilhem o cache.


Pronto para adicionar a camada SMTP? O plano gratuito inclui 100 créditos que rodam o pipeline completo, e a referência da API tem os endpoints /verify e /jobs completos, com exemplos de copiar e colar em sete linguagens.

Your reputation, protected.

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

Get started