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

Validar um endereço de e-mail no Next.js

8 minutes read

Qualisend team
Uma janela de editor de código intitulada route.ts mostrando três camadas de validação do Next.js que vão afunilando — um schema de e-mail zod, uma consulta MX e uma chamada à API de verificação — terminando em um selo verde de entregável.

Para validar um endereço de e-mail no Next.js você precisa responder três perguntas, e o App Router lhe dá um lugar limpo para responder cada uma. O endereço tem o formato correto? O domínio dele consegue receber e-mail? A caixa postal realmente existe? A primeira é um schema zod que você compartilha entre um Client Component e o servidor; a segunda é uma consulta de DNS; a terceira é um problema de rede que você delega a uma API de verificação. A armadilha é parar na primeira pergunta — uma aprovação de z.string().email() no navegador é uma gentileza de UX, não validação, e ela nunca chega intacta ao seu banco de dados, porque qualquer um pode dar um POST passando reto por ela.

A resposta curta#

Valide com um schema zod compartilhado para a sintaxe tanto no cliente quanto no servidor, node:dns/promises para a consulta MX dentro de um Route Handler, e uma API de verificação para a checagem de caixa postal por SMTP — o mais barato primeiro, encerrando o processo assim que uma camada for decisiva. Mantenha no servidor toda verificação que importa: a cópia no cliente serve para retorno instantâneo, e a chave de API que autentica a sondagem de caixa postal jamais pode ir para o navegador. Não tente abrir conexões SMTP a partir de uma função do Next.js para sondar caixas postais você mesmo — a porta 25 de saída fica bloqueada na maioria dos hosts (e é totalmente indisponível no runtime Edge), e a resposta depende de reputação de IP de envio e de greylisting que você não vai querer reimplementar.

Camada 1: validação de formato com um schema zod compartilhado#

O Next.js não tem uma primitiva de e-mail própria, mas a checagem de formato idiomática é um schema zod, e a razão pela qual o zod encaixa tão bem aqui é que o mesmo schema roda no navegador e no servidor. Defina-o uma vez:

// lib/email.ts
import { z } from "zod";

export const signupSchema = z.object({
  email: z.string().email("Enter a valid email address."),
});

Em um Client Component, faça o parse contra ele no envio para que o usuário receba um erro instantâneo sem uma ida e volta ao servidor. O safeParse retorna um resultado discriminado que você pode ler sem um try/catch:

"use client";

import { useState } from "react";
import { signupSchema } from "@/lib/email";

export function SignupForm() {
  const [error, setError] = useState<string | null>(null);

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const email = String(new FormData(e.currentTarget).get("email"));

    // Instant feedback only — this is not a security boundary.
    const parsed = signupSchema.safeParse({ email });
    if (!parsed.success) {
      setError(parsed.error.issues[0].message);
      return;
    }

    const res = await fetch("/api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email }),
    });
    setError(res.ok ? null : (await res.json()).error);
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" autoComplete="email" required />
      {error && <p role="alert">{error}</p>}
      <button type="submit">Sign up</button>
    </form>
  );
}

A checagem no cliente vale a pena — ela evita uma requisição em erros óbvios de digitação — mas é o oposto de confiável. Um usuário com a aba de rede aberta pode dar um POST direto para /api/signup e ignorá-la por completo, então o servidor precisa rodar exatamente o mesmo schema de novo antes de acreditar em qualquer coisa.

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

Esta é a primeira checagem que o navegador não consegue fazer e que o zod não vai fazer: um domínio sem registros MX não consegue aceitar e-mail para ninguém, então uma consulta de DNS elimina domínios mortos, nomes de empresas digitados errado e TLDs inventados. Um Route Handler do Next.js roda no Node por padrão, então você tem node:dns/promises sem nenhuma dependência:

// lib/email.ts (continued)
import { resolveMx } from "node:dns/promises";

export async function hasMailRoute(domain: string): Promise<boolean> {
  try {
    const mx = await resolveMx(domain);
    return mx.length > 0;
  } catch {
    // ENOTFOUND / ENODATA → the domain doesn't exist or publishes no MX.
    return false;
  }
}

Uma aprovação aqui significa "o domínio aceita e-mail", não "esta caixa postal existe" — mas uma reprovação é decisiva e gratuita, que é exatamente o que você quer de uma camada barata.

Camada 3: a caixa postal realmente existe?#

As camadas 1 e 2 só conseguem descartar 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 tem sintaxe válida em uma rota de e-mail ativa, e mesmo assim é uma caixa postal que nunca foi criada. Confirmar uma caixa postal específica significa a conversa de entrega SMTP: conectar ao host de e-mail, emitir RCPT TO, ler a resposta e desconectar antes de enviar qualquer coisa.

Você poderia programar isso a partir de um runtime Node, mas não deveria executá-lo a partir de uma função do Next.js. 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ê conecta, e os servidores de recebimento fazem greylisting e limitam a taxa de remetentes desconhecidos — então uma sondagem que funciona localmente falha silenciosamente, ou faz você entrar em uma blocklist, em produção. Como funciona a verificação de e-mail percorre o pipeline completo, domínios catch-all e tudo mais. Esta é a camada que vale a pena delegar.

O endpoint de verificação da Qualisend roda o pipeline inteiro a partir de uma infraestrutura com reputação gerenciada e retorna um veredito. Chame-o no lado do servidor com fetch, lendo a chave de uma variável de ambiente para que ela nunca saia da máquina:

// lib/email.ts (continued)
export async function verifyMailbox(email: string): Promise<string> {
  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 }),
  });

  // Don't block a real signup on our outage — treat a failure as "unknown".
  if (!res.ok) return "unknown";

  const { result } = await res.json();
  return result.status; // "deliverable" | "risky" | "undeliverable" | "unknown"
}

A resposta é um envelope { "result": { ... } }. Você lê result.status junto de um score, um reason e sub_flags para características como endereços de função (role) ou descartáveis:

{
  "result": {
    "status": "deliverable",
    "score": 95,
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
  }
}

Consulte a referência da API para os formatos exatos dos campos; o helper acima só precisa de status para rejeitar um endereço que não vai ser entregue. Repare que a variável de ambiente é simplesmente QUALISEND_API_KEY, sem o prefixo NEXT_PUBLIC_ — esse prefixo é o que embute um valor no bundle do cliente, e uma chave de verificação no navegador é uma chave que qualquer um pode ler e gastar.

Compondo as camadas para validar um endereço de e-mail no Next.js#

Agora costure os três helpers em um único route.ts, o mais barato primeiro, para que o crédito de API só seja gasto em um endereço que já passou pela sintaxe e pela verificação MX:

// app/api/signup/route.ts
import { NextResponse } from "next/server";
import { signupSchema, hasMailRoute, verifyMailbox } from "@/lib/email";

export async function POST(request: Request) {
  // Layer 1 — re-validate on the server; the client check is not a boundary.
  const parsed = signupSchema.safeParse(await request.json());
  if (!parsed.success) {
    return NextResponse.json({ error: "Enter a valid email address." }, { status: 400 });
  }

  const { email } = parsed.data;
  const domain = email.slice(email.lastIndexOf("@") + 1);

  // Layer 2 — cheap MX gate, rules out dead domains before any API call.
  if (!(await hasMailRoute(domain))) {
    return NextResponse.json({ error: "That domain can't receive email." }, { status: 400 });
  }

  // Layer 3 — the SMTP mailbox probe, delegated to Qualisend.
  if ((await verifyMailbox(email)) === "undeliverable") {
    return NextResponse.json(
      { error: "We couldn't confirm a mailbox at this address." },
      { status: 400 },
    );
  }

  // Every layer passed (or the probe was inconclusive) — create the account.
  // await createUser(email);
  return NextResponse.json({ ok: true });
}

Essa ordenação é o truque inteiro: o schema descarta o lixo, a checagem local de DNS elimina domínios mortos de graça, e a API só é acionada para endereços que passaram pelas duas. O handler só reprova de forma definitiva o undeliverable e deixa risky e unknown passarem, então um endereço limítrofe ou uma falha transitória da API nunca afasta um cliente de verdade — você pode ramificar com base no score ou em uma entrada de sub_flags mais adiante, em vez de bloquear o cadastro.

Se você preferir enviar o formulário sem escrever código de fetch no cliente, o corpo idêntico funciona em uma Server Action. Retorne { error } em vez de NextResponse e consuma-o com useActionState:

"use server";

import { signupSchema, hasMailRoute, verifyMailbox } from "@/lib/email";

export async function signup(_prev: unknown, formData: FormData) {
  const parsed = signupSchema.safeParse({ email: formData.get("email") });
  if (!parsed.success) return { error: "Enter a valid email address." };

  const { email } = parsed.data;
  const domain = email.slice(email.lastIndexOf("@") + 1);
  if (!(await hasMailRoute(domain))) return { error: "That domain can't receive email." };

  if ((await verifyMailbox(email)) === "undeliverable") {
    return { error: "We couldn't confirm a mailbox at this address." };
  }
  return { ok: true };
}

Este é o mesmo formato de três camadas que você vai encontrar no guia do Node.js — é a estratificação, não o framework, que torna a validação confiável. Rode este pipeline rápido de forma síncrona no cadastro e reserve verificações em lote mais profundas para a limpeza de listas; o guia de cadastro serverless mostra o padrão em tempo real de ponta a ponta, e a comparação de APIs alinha os serviços que podem sustentar a camada três.

Perguntas frequentes#

z.string().email() é suficiente para validar um endereço de e-mail no Next.js?#

Para a sintaxe, sim — z.string().email() é a verificação certa para a camada um e muito melhor do que um regex feito à mão. Mas ela valida o formato: nunca resolve o DNS nem contata um servidor de e-mail, então uma aprovação significa "parece um e-mail", não "vai ser entregue". E uma verificação feita apenas no navegador é burlada trivialmente ao enviar uma requisição diretamente para a sua rota. Rode o schema novamente no servidor e depois acrescente uma consulta MX e uma verificação de caixa postal por SMTP antes de confiar no endereço.

Onde eu coloco a chave de API da Qualisend em um app Next.js?#

Em uma variável de ambiente exclusiva do servidor chamada QUALISEND_API_KEY — nunca em uma com o prefixo NEXT_PUBLIC_. O prefixo NEXT_PUBLIC_ embute um valor no bundle do cliente, então uma chave de verificação com esse prefixo pode ser lida por qualquer pessoa que abrir o seu site. Leia process.env.QUALISEND_API_KEY dentro de um Route Handler ou de uma Server Action, onde ela permanece no servidor, e deixe o navegador chamar o seu próprio endpoint em vez de chamar a Qualisend diretamente.

Devo validar um e-mail no cliente ou no servidor no Next.js?#

Nos dois, mas só o servidor conta. Rode o schema zod compartilhado no Client Component para dar um retorno instantâneo sobre erros de digitação e rode exatamente o mesmo schema de novo no Route Handler ou na Server Action, porque a verificação no cliente pode ser totalmente ignorada. As camadas de DNS e SMTP são exclusivas do servidor por natureza — precisam de APIs do Node e de uma chave secreta — então o navegador nunca as enxerga.

Posso rodar a verificação de DNS/MX no runtime Edge?#

Não. node:dns/promises exige o runtime do Node.js, que os Route Handlers usam por padrão; uma rota com export const runtime = "edge" não tem o módulo dns. Se você precisa do Edge, dispense o gate local de MX e deixe a Qualisend resolver o DNS como parte da chamada de verificação — o pipeline dela já faz a consulta MX e a sondagem SMTP, então a única chamada de API cobre as camadas dois e três.


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 zod voltar como undeliverable, e depois integre o mesmo veredito ao seu Route Handler com os exemplos de copiar e colar da referência da API.

Your reputation, protected.

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

Get started