Bloquez les e-mails jetables dans Next.js avec un seul fichier middleware
Un middleware Next.js de 40 lignes qui appelle l'API botoi pour rejeter les inscriptions à partir d'adresses e-mail temporaires. Copiez, collez, déployez.
Un utilisateur s'inscrit avec test92847@mailinator.com, brûle votre essai gratuit et disparaît.
Ils reviennent demain avec test92848@mailinator.com et recommencez.
Votre file d'attente d'assistance se remplit de comptes fantômes. Vos analyses montrent un nombre d’utilisateurs gonflé qui ne veut rien dire.
Votre détection d'abus se déclenche trop tard car le compte a déjà consommé des ressources.
La solution : bloquez les e-mails jetables à la porte, avant que la demande d'inscription n'atteigne votre base de données. Ce guide montre comment procéder dans Next.js avec un seul fichier middleware et aucune dépendance au-delà d'un appel de récupération.
Ce que vous construirez
Un middleware Next.js qui intercepte les requêtes POST vers votre point de terminaison d'inscription, extrait l'e-mail du corps de la demande, le vérifie par rapport au API de messagerie jetable Botoi, et renvoie une réponse 422 si l'e-mail appartient à un service jetable. L'ensemble du fichier fait moins de 50 lignes.
Le middleware
Créer middleware.ts à la racine de votre projet (ou src/middleware.ts si vous utilisez le src annuaire):
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'],
};
Comment ça marche
Correspondance d'itinéraire
La config.matcher array indique à Next.js quelles routes déclenchent ce middleware.
Modifiez ces chemins pour qu'ils correspondent à vos points de terminaison d'inscription. Le middleware s'exécute en périphérie avant que votre gestionnaire de route ne s'exécute,
les demandes rejetées ne touchent donc jamais votre base de données ou votre fournisseur d'authentification.
Extraction d'e-mails
Le middleware lit le corps de la requête avec req.json() et tire le email champ.
Si l’analyse échoue ou qu’aucun e-mail n’existe, la demande est transmise sans modification.
Cela maintient le middleware invisible pour les routes sans inscription.
L'appel API
Un seul POST à https://api.botoi.com/v1/disposable-email/check avec l'e-mail dans le corps.
La réponse comprend :
{
"success": true,
"data": {
"email": "throwaway@mailinator.com",
"domain": "mailinator.com",
"is_disposable": true,
"is_free": false,
"provider": "Mailinator"
}
}
La is_disposable le drapeau est la porte. Quand c'est true, le middleware renvoie un 422 avec un message clair.
Quand c'est false, NextResponse.next() laisse la demande continuer vers votre gestionnaire d'inscription.
Conception à ouverture en cas de panne
La catch bloquer l'appel de récupération signifie que les pannes de réseau, les délais d'attente ou les temps d'arrêt de l'API n'interrompent pas les inscriptions.
Le middleware enregistre un avertissement et laisse passer la demande. Vos utilisateurs ne voient jamais d’erreur causée par une panne tierce.
Gestion des cas extrêmes
Délais d'attente
L'API botoi répond en moins de 50 ms pour la plupart des requêtes car elle utilise une liste de domaines en mémoire.
Si vous voulez un délai d'attente difficile, enveloppez la récupération 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 taux
Le niveau gratuit autorise 5 requêtes par minute. Si votre application traite plus d'inscriptions que cela, obtenir une clé API et transmettez-le comme jeton de porteur :
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${process.env.BOTOI_API_KEY}\`,
},
body: JSON.stringify({ email }),
});
Magasin BOTOI_API_KEY dans votre .env.local déposer. Ne le confiez jamais au contrôle de version.
Chèques en double
Si le même e-mail arrive deux fois de suite sur votre point de terminaison d'inscription (logique de double-clic, réessayez), vous effectuerez deux appels API pour le même domaine. Pour la plupart des applications, c'est très bien. Si cela est important, ajoutez un cache de courte durée (décrit ci-dessous).
Durcissement de production
Ajouter un cache en mémoire
Mettez en cache le résultat du contrôle jetable par domaine pendant 5 minutes. Cela réduit les appels d'API et accélère les vérifications répétées pour le même domaine :
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
}
}
Ce cache basé sur une carte fonctionne dans les environnements d'exécution sans serveur et en périphérie. Pour les déploiements multi-instances, remplacez-le par 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;
}
}
Ajouter des domaines d'entreprise à la liste autorisée
Certaines entreprises utilisent des domaines personnalisés que vous ne souhaitez jamais bloquer, même s'ils correspondent à un modèle suspect. Ajoutez une liste verte :
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
}
Consigner les tentatives bloquées
Suivez les domaines rejetés afin de pouvoir surveiller les modèles d'abus et ajuster votre stratégie :
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 complet avec tout le durcissement
Voici le fichier complet avec mise en cache, liste blanche, délai d'attente et journalisation structurée :
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'],
};
Le tester localement
Démarrez votre serveur de développement Next.js et lancez une requête avec un e-mail jetable connu :
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "test@mailinator.com", "password": "hunter2"}'
Réponse attendue :
{
"error": "Disposable email addresses are not allowed. Please use a permanent email."
}
Essayez un e-mail légitime pour confirmer qu'il est transmis :
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "dev@acme-corp.com", "password": "hunter2"}'
Cette demande parvient normalement à votre gestionnaire d'inscription.
Quand vérifier aussi le client
Le middleware récupère les e-mails jetables sur le serveur. Mais vous pouvez également appeler la même API depuis votre formulaire d'inscription pour afficher une erreur en ligne *avant* que l'utilisateur soumette. Une vérification rapide côté client après que le champ de courrier électronique ait perdu le focus évite à l'utilisateur un aller-retour et donne un retour d'information plus rapide :
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
}
Le middleware fait toujours office de porte faisant autorité. La vérification côté client est une amélioration de l’UX et non une mesure de sécurité.
FAQ
- Cela fonctionne-t-il avec le routeur d'application Next.js ?
- Oui. Le middleware Next.js s'exécute avant tout gestionnaire de route, que vous utilisiez le routeur d'applications ou le routeur de pages. Le fichier middleware.ts se trouve à la racine du projet (ou à l'intérieur de src/ si vous utilisez le répertoire src) et il intercepte les requêtes vers les chemins correspondants avant qu'elles n'atteignent vos routes API ou vos actions de serveur.
- Ai-je besoin d’une clé API pour la vérification des e-mails jetables botoi ?
- Non. L’offre gratuite autorise 5 requêtes par minute sans clé API. Pour les applications de production gérant davantage d'inscriptions, récupérez une clé sur la page de documentation de l'API botoi pour débloquer des limites de débit plus élevées.
- Que se passe-t-il si l'API botoi est en panne ?
- Le middleware détecte les erreurs réseau et laisse passer la demande. Cette approche d'ouverture par échec signifie qu'une panne temporaire de l'API n'empêche jamais les utilisateurs légitimes de s'inscrire. Vous pouvez ajouter une journalisation pour suivre le moment où le comportement de secours entre en jeu.
- Puis-je l'utiliser avec d'autres frameworks comme Remix ou SvelteKit ?
- L'appel API lui-même fonctionne à partir de n'importe quel environnement côté serveur. Le modèle de middleware présenté ici est spécifique à Next.js, mais la logique de base (POST vers le point de terminaison, vérifiez is_disposable dans la réponse) se traduit directement par des chargeurs Remix, des hooks SvelteKit ou un middleware Express.
- Quelle est la précision de la détection des e-mails jetables ?
- Le point de terminaison vérifie une liste de plus de 700 domaines jetables connus et utilise la correspondance de modèles pour détecter les variations. Il identifie également les fournisseurs de messagerie gratuits (Gmail, Outlook, Yahoo) séparément des fournisseurs jetables, afin que vous puissiez faire la distinction entre une adresse Gmail personnelle et une adresse Mailinator jetable.
Commencez a construire avec botoi
150+ endpoints API pour la recherche, le traitement de texte, la generation d'images et les utilitaires pour developpeurs. Offre gratuite, sans carte bancaire.