Ir al contenido
Integration

Bloquee correos electrónicos desechables en Next.js con un archivo de middleware

| 6 min read

Un middleware Next.js de 40 líneas que llama a la API botoi para rechazar registros de direcciones de correo electrónico temporales. Copiar, pegar, implementar.

Email inbox interface on a laptop
Photo by Stephen Phillips on Unsplash

Una usuaria se registra con test92847@mailinator.com, agota su prueba gratuita y desaparece. Ellos regresan mañana con test92848@mailinator.com y hazlo de nuevo. Su cola de soporte se llena de cuentas fantasmas. Sus análisis muestran recuentos de usuarios inflados que no significan nada. Su detección de abuso se activa demasiado tarde porque la cuenta ya consumió recursos.

La solución: bloquear los correos electrónicos desechables en la puerta, antes de que la solicitud de registro llegue a su base de datos. Esta guía muestra cómo hacerlo en Next.js con un único archivo de middleware y cero dependencias más allá de una llamada de recuperación.

lo que construirás

Un middleware Next.js que intercepta solicitudes POST a su punto final de registro. extrae el correo electrónico del cuerpo de la solicitud, lo compara con el API de correo electrónico desechable de botoi, y devuelve una respuesta 422 si el correo electrónico pertenece a un servicio desechable. El archivo completo tiene menos de 50 líneas.

La middleware

Crear middleware.ts en la raíz de su proyecto (o src/middleware.ts si usas el src directorio):

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

Coincidencia de ruta

La config.matcher La matriz le dice a Next.js qué rutas activan este middleware. Cambie estas rutas para que coincidan con sus puntos finales de registro. El middleware se ejecuta en el borde antes de que se ejecute el controlador de ruta, por lo tanto, las solicitudes rechazadas nunca tocan su base de datos o proveedor de autenticación.

Extracción de correo electrónico

El middleware lee el cuerpo de la solicitud con req.json() y tira el email campo. Si el análisis falla o no existe ningún correo electrónico, la solicitud pasa intacta. Esto mantiene el middleware invisible para las rutas sin registro.

La llamada API

Un solo POST para https://api.botoi.com/v1/disposable-email/check con el correo electrónico en el cuerpo. La respuesta incluye:

{
  "success": true,
  "data": {
    "email": "throwaway@mailinator.com",
    "domain": "mailinator.com",
    "is_disposable": true,
    "is_free": false,
    "provider": "Mailinator"
  }
}

La is_disposable La bandera es la puerta. cuando es true, el middleware devuelve un 422 con un mensaje claro. cuando es false, NextResponse.next() permite que la solicitud continúe hasta su controlador de registro.

Diseño a prueba de fallos

La catch bloquear alrededor de la llamada de recuperación significa que las fallas de red, los tiempos de espera o el tiempo de inactividad de API no interrumpen los registros. El middleware registra una advertencia y deja pasar la solicitud. Sus usuarios nunca ven un error causado por una interrupción de un tercero.

Manejo de casos extremos

Tiempos de espera

La API de botoi responde en menos de 50 ms para la mayoría de las solicitudes, ya que utiliza una lista de dominios en memoria. Si desea un tiempo de espera difícil, finalice la búsqueda 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
});

Límites de tarifas

El nivel gratuito permite 5 solicitudes por minuto. Si su aplicación procesa más registros que eso, obtener una clave API Y pasarlo como ficha de portadora:

const res = await fetch(BOTOI_URL, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': \`Bearer \${process.env.BOTOI_API_KEY}\`,
  },
  body: JSON.stringify({ email }),
});

Almacenar BOTOI_API_KEY en tu .env.local archivo. Nunca lo comprometas con el control de versiones.

Cheques duplicados

Si el mismo correo electrónico llega a su punto final de registro dos veces en rápida sucesión (doble clic, reintento lógico), Realizarás dos llamadas API para el mismo dominio. Para la mayoría de las aplicaciones esto está bien. Si es importante, agregue un caché de corta duración (que se explica a continuación).

Endurecimiento de producción

Agregar un caché en memoria

Almacene en caché el resultado de la verificación desechable por dominio durante 5 minutos. Esto reduce las llamadas a la API y acelera las comprobaciones repetidas para el mismo dominio:

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 caché basado en mapas funciona en tiempos de ejecución perimetrales y sin servidor. Para implementaciones de múltiples instancias, cámbielo por Redis o 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 dominios corporativos permitidos

Algunas empresas utilizan dominios personalizados que usted nunca desea bloquear, incluso si coinciden con un patrón sospechoso. Agregar una lista de permitidos:

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 intentos bloqueados

Realice un seguimiento de los dominios rechazados para poder controlar los patrones de abuso y ajustar su estrategia:

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 con todo el refuerzo

Aquí está el archivo completo con almacenamiento en caché, listas de permitidos, tiempo de espera y registro estructurado:

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'],
};

Probándolo localmente

Inicie su servidor de desarrollo Next.js y active una solicitud con un correo electrónico desechable conocido:

curl -X POST http://localhost:3000/api/auth/signup \\
  -H "Content-Type: application/json" \\
  -d '{"email": "test@mailinator.com", "password": "hunter2"}'

Respuesta esperada:

{
  "error": "Disposable email addresses are not allowed. Please use a permanent email."
}

Pruebe con un correo electrónico legítimo para confirmar que pasa:

curl -X POST http://localhost:3000/api/auth/signup \\
  -H "Content-Type: application/json" \\
  -d '{"email": "dev@acme-corp.com", "password": "hunter2"}'

Esta solicitud llega a su controlador de registro normalmente.

Cuando revisar a la cliente también

El middleware captura correos electrónicos desechables en el servidor. Pero también puedes llamar a la misma API desde tu formulario de registro. para mostrar un error en línea *antes* de que el usuario envíe. Una verificación rápida del lado del cliente después de que el campo de correo electrónico pierde el foco le ahorra al usuario un viaje de ida y vuelta y brinda retroalimentación más rápida:

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
}

El middleware sigue actuando como puerta de autoridad. La verificación del lado del cliente es una mejora de UX, no una medida de seguridad.

FAQ

¿Funciona esto con el enrutador de aplicaciones Next.js?
Sí. El middleware Next.js se ejecuta antes que cualquier controlador de ruta, independientemente de si utiliza App Router o Pages Router. El archivo middleware.ts se encuentra en la raíz del proyecto (o dentro de src/ si usa el directorio src) e intercepta las solicitudes a las rutas coincidentes antes de que lleguen a las rutas API o acciones del servidor.
¿Necesito una clave API para la verificación de correo electrónico desechable de botoi?
No. El nivel gratuito permite 5 solicitudes por minuto sin clave API. Para aplicaciones de producción que manejan más registros, obtenga una clave de la página de documentos de la API de botoi para desbloquear límites de velocidad más altos.
¿Qué sucede si la API de botoi no funciona?
El middleware detecta errores de red y deja pasar la solicitud. Este enfoque de apertura fallida significa que una interrupción temporal de la API nunca impide que los usuarios legítimos se registren. Puede agregar registros para realizar un seguimiento cuando se activa el comportamiento alternativo.
¿Puedo usar esto con otros frameworks como Remix o SvelteKit?
La llamada API en sí funciona desde cualquier entorno del lado del servidor. El patrón de middleware que se muestra aquí es específico de Next.js, pero la lógica central (POST en el punto final, marque is_disposable en la respuesta) se traduce directamente en cargadores Remix, ganchos SvelteKit o middleware Express.
¿Qué tan precisa es la detección de correo electrónico desechable?
El punto final compara con una lista de más de 700 dominios desechables conocidos y utiliza la coincidencia de patrones para detectar variaciones. También identifica proveedores de correo electrónico gratuitos (Gmail, Outlook, Yahoo) por separado de los desechables, para que puedas distinguir entre una dirección de Gmail personal y una dirección de Mailinator desechable.

Empieza a construir con botoi

150+ endpoints de API para consultas, procesamiento de texto, generacion de imagenes y utilidades para desarrolladores. Plan gratuito, sin tarjeta de credito.