Bloqueie e-mails descartáveis em Next.js com um arquivo de middleware
Um middleware Next.js de 40 linhas que chama a API botoi para rejeitar inscrições de endereços de e-mail temporários. Copie, cole, implante.
Um usuário se inscreve com test92847@mailinator.com, esgota sua avaliação gratuita e desaparece.
Eles voltam amanhã com test92848@mailinator.com e faça de novo.
Sua fila de suporte fica cheia de contas fantasmas. Suas análises mostram contagens de usuários inflacionadas que não significam nada.
A detecção de abuso é acionada tarde demais porque a conta já consumiu recursos.
A solução: bloqueie e-mails descartáveis na porta, antes que a solicitação de inscrição chegue ao seu banco de dados. Este guia mostra como fazer isso em Next.js com um único arquivo de middleware e zero dependências além de uma chamada de busca.
O que você construirá
Um middleware Next.js que intercepta solicitações POST para seu endpoint de inscrição, extrai o e-mail do corpo da solicitação, compara-o com o API de e-mail descartável botoi, e retorna uma resposta 422 se o e-mail pertencer a um serviço descartável. O arquivo inteiro tem menos de 50 linhas.
O intermediário
Criar middleware.ts na raiz do seu projeto (ou src/middleware.ts se você usar o src diretório):
import { NextRequest, NextResponse } from 'next/server';
const BOTOI_URL = 'https://api.botoi.com/v1/disposable-email/check';
export async function middleware(req: NextRequest) {
// Only intercept POST requests to the signup route
if (req.method !== 'POST') {
return NextResponse.next();
}
let body: { email?: string };
try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: 'Invalid request body' },
{ status: 400 }
);
}
const email = body.email?.trim().toLowerCase();
if (!email) {
return NextResponse.next();
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (data.success && data.data.is_disposable) {
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed. Please use a permanent email.' },
{ status: 422 }
);
}
} catch {
// API unreachable; fail open so real users aren't blocked
console.warn('botoi disposable-email check failed, allowing request through');
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/auth/signup', '/api/register'],
};
Como funciona
Correspondência de rota
O config.matcher array informa ao Next.js quais rotas acionam esse middleware.
Altere esses caminhos para corresponder aos seus endpoints de inscrição. O middleware é executado no limite antes da execução do manipulador de rota,
portanto, solicitações rejeitadas nunca afetam seu banco de dados ou provedor de autenticação.
Extração de e-mail
O middleware lê o corpo da solicitação com req.json() e puxa o email campo.
Se a análise falhar ou não existir nenhum email, a solicitação passará intacta.
Isso mantém o middleware invisível para rotas sem inscrição.
A chamada da API
Um único POST para https://api.botoi.com/v1/disposable-email/check com o e-mail no corpo.
A resposta inclui:
{
"success": true,
"data": {
"email": "throwaway@mailinator.com",
"domain": "mailinator.com",
"is_disposable": true,
"is_free": false,
"provider": "Mailinator"
}
}
O is_disposable bandeira é o portão. Quando é true, o middleware retorna um 422 com uma mensagem clara.
Quando é false, NextResponse.next() permite que a solicitação continue no seu manipulador de inscrição.
Design de falha aberta
O catch bloquear a chamada de busca significa que falhas de rede, tempos limite ou tempo de inatividade da API não interrompem as inscrições.
O middleware registra um aviso e permite a solicitação. Seus usuários nunca veem um erro causado por uma interrupção de terceiros.
Lidando com casos extremos
Tempos limite
A API botoi responde em menos de 50 ms para a maioria das solicitações, pois usa uma lista de domínios na memória.
Se você quiser um tempo limite difícil, envolva a busca AbortSignal.timeout():
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000), // 3 second timeout
});
Limites de taxa
O nível gratuito permite 5 solicitações por minuto. Se seu aplicativo processar mais inscrições do que isso, obter uma chave API e passe-o como um token de portador:
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${process.env.BOTOI_API_KEY}\`,
},
body: JSON.stringify({ email }),
});
Loja BOTOI_API_KEY em seu .env.local arquivo. Nunca o comprometa com o controle de versão.
Verificações duplicadas
Se o mesmo e-mail chegar ao seu endpoint de inscrição duas vezes em rápida sucessão (clique duas vezes, tente novamente a lógica), você fará duas chamadas de API para o mesmo domínio. Para a maioria dos aplicativos, isso é bom. Se for importante, adicione um cache de curta duração (abordado abaixo).
Endurecimento de produção
Adicione um cache na memória
Armazene em cache o resultado da verificação descartável por domínio por 5 minutos. Isso reduz as chamadas de API e acelera verificações repetidas para o mesmo domínio:
const cache = new Map<string, { isDisposable: boolean; expires: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
const cached = cache.get(domain);
if (cached && cached.expires > Date.now()) {
return cached.isDisposable;
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
cache.set(domain, {
isDisposable,
expires: Date.now() + CACHE_TTL,
});
return isDisposable;
} catch {
return false; // fail open
}
}
Este cache baseado em mapa funciona em tempos de execução sem servidor e de borda. Para implantações de várias instâncias, troque por Redis ou Upstash:
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
const cached = await redis.get<boolean>(\`disposable:\${domain}\`);
if (cached !== null) {
return cached;
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
await redis.set(\`disposable:\${domain}\`, isDisposable, { ex: 300 });
return isDisposable;
} catch {
return false;
}
}
Listar domínios corporativos na lista de permissões
Algumas empresas usam domínios personalizados que você nunca deseja bloquear, mesmo que correspondam a um padrão suspeito. Adicione uma lista de permissões:
const ALLOWED_DOMAINS = new Set([
'yourcompany.com',
'partner-corp.io',
'bigclient.co',
]);
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
if (ALLOWED_DOMAINS.has(domain)) {
return false;
}
// ... rest of the check logic
}
Registrar tentativas bloqueadas
Acompanhe quais domínios são rejeitados para monitorar padrões de abuso e ajustar sua estratégia:
if (data.success && data.data.is_disposable) {
console.log(
JSON.stringify({
event: 'disposable_email_blocked',
domain: data.data.domain,
provider: data.data.provider,
timestamp: new Date().toISOString(),
})
);
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed.' },
{ status: 422 }
);
}
Middleware completo com todo o hardening
Aqui está o arquivo completo com cache, lista de permissões, tempo limite e registro estruturado:
import { NextRequest, NextResponse } from 'next/server';
const BOTOI_URL = 'https://api.botoi.com/v1/disposable-email/check';
const CACHE_TTL = 5 * 60 * 1000;
const ALLOWED_DOMAINS = new Set([
// Add your corporate or partner domains here
]);
const cache = new Map<string, { isDisposable: boolean; expires: number }>();
async function checkDisposable(email: string): Promise<{
isDisposable: boolean;
domain: string;
provider: string | null;
}> {
const domain = email.split('@')[1];
if (ALLOWED_DOMAINS.has(domain)) {
return { isDisposable: false, domain, provider: null };
}
const cached = cache.get(domain);
if (cached && cached.expires > Date.now()) {
return { isDisposable: cached.isDisposable, domain, provider: null };
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
const provider = data.data?.provider ?? null;
cache.set(domain, { isDisposable, expires: Date.now() + CACHE_TTL });
return { isDisposable, domain, provider };
} catch {
return { isDisposable: false, domain, provider: null };
}
}
export async function middleware(req: NextRequest) {
if (req.method !== 'POST') {
return NextResponse.next();
}
let body: { email?: string };
try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: 'Invalid request body' },
{ status: 400 }
);
}
const email = body.email?.trim().toLowerCase();
if (!email || !email.includes('@')) {
return NextResponse.next();
}
const result = await checkDisposable(email);
if (result.isDisposable) {
console.log(
JSON.stringify({
event: 'disposable_email_blocked',
domain: result.domain,
provider: result.provider,
timestamp: new Date().toISOString(),
})
);
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed. Please use a permanent email.' },
{ status: 422 }
);
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/auth/signup', '/api/register'],
};
Testando localmente
Inicie seu servidor de desenvolvimento Next.js e dispare uma solicitação com um e-mail descartável conhecido:
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "test@mailinator.com", "password": "hunter2"}'
Resposta esperada:
{
"error": "Disposable email addresses are not allowed. Please use a permanent email."
}
Tente um e-mail legítimo para confirmar que ele foi enviado:
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "dev@acme-corp.com", "password": "hunter2"}'
Essa solicitação chega ao seu manipulador de inscrição normalmente.
Quando verificar o cliente também
O middleware captura e-mails descartáveis no servidor. Mas você também pode chamar a mesma API no seu formulário de inscrição para mostrar um erro embutido *antes* do usuário enviar. Uma rápida verificação do lado do cliente depois que o campo de e-mail perde o foco economiza ao usuário uma viagem de ida e volta e fornece feedback mais rápido:
async function validateEmail(email: string): Promise<string | null> {
const res = await fetch('https://api.botoi.com/v1/disposable-email/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (data.success && data.data.is_disposable) {
return 'Please use a permanent email address.';
}
return null; // no error
}
O middleware ainda atua como porta de autoridade. A verificação do lado do cliente é uma melhoria de UX, não uma medida de segurança.
FAQ
- Isso funciona com o roteador de aplicativos Next.js?
- Sim. O middleware Next.js é executado antes de qualquer manipulador de rota, independentemente de você usar o App Router ou o Pages Router. O arquivo middleware.ts fica na raiz do projeto (ou dentro de src/ se você usar o diretório src) e intercepta solicitações para os caminhos correspondentes antes que eles alcancem suas rotas de API ou ações do servidor.
- Preciso de uma chave de API para a verificação de e-mail descartável do botoi?
- Não. O nível gratuito permite cinco solicitações por minuto sem chave de API. Para aplicativos de produção que lidam com mais inscrições, pegue uma chave na página de documentos da API do botoi para desbloquear limites de taxa mais altos.
- O que acontece se a API do botoi estiver inoperante?
- O middleware detecta erros de rede e permite a passagem da solicitação. Essa abordagem de falha aberta significa que uma interrupção temporária da API nunca impede que usuários legítimos se inscrevam. Você pode adicionar registros para rastrear quando o comportamento de fallback entra em ação.
- Posso usar isso com outros frameworks como Remix ou SvelteKit?
- A própria chamada da API funciona em qualquer ambiente do lado do servidor. O padrão de middleware mostrado aqui é específico do Next.js, mas a lógica central (POST para o endpoint, verifique is_disposable na resposta) se traduz diretamente em carregadores Remix, ganchos SvelteKit ou middleware Express.
- Quão precisa é a detecção de e-mail descartável?
- O endpoint verifica uma lista de mais de 700 domínios descartáveis conhecidos e usa correspondência de padrões para capturar variações. Ele também identifica provedores de e-mail gratuitos (Gmail, Outlook, Yahoo) separadamente dos descartáveis, para que você possa distinguir entre um endereço Gmail pessoal e um endereço descartável do Mailinator.
Comece a construir com botoi
150+ endpoints de API para consultas, processamento de texto, geração de imagens e utilitários para desenvolvedores. Plano gratuito, sem cartão de crédito.