Blokir email sekali pakai di Next.js dengan satu file middleware
Middleware Next.js 40 baris yang memanggil API botoi untuk menolak pendaftaran dari alamat email sementara. Salin, tempel, terapkan.
Seorang pengguna mendaftar test92847@mailinator.com, menghabiskan uji coba gratis Anda, dan menghilang.
Mereka kembali besok bersama test92848@mailinator.com dan melakukannya lagi.
Antrean dukungan Anda dipenuhi dengan akun hantu. Analisis Anda menunjukkan peningkatan jumlah pengguna yang tidak berarti apa-apa.
Deteksi penyalahgunaan Anda terlambat dilakukan karena akun telah menghabiskan sumber daya.
Cara mengatasinya: blokir email sekali pakai di depan pintu, sebelum permintaan pendaftaran mencapai database Anda. Panduan ini menunjukkan cara melakukannya di Next.js dengan satu file middleware dan tidak ada ketergantungan selain panggilan pengambilan.
Apa yang akan Anda bangun
Middleware Next.js yang mencegat permintaan POST ke titik akhir pendaftaran Anda, mengekstrak email dari badan permintaan, memeriksanya dengan botoi disposable email API, dan mengembalikan respons 422 jika email tersebut milik layanan sekali pakai. Seluruh file berada di bawah 50 baris.
Perangkat tengah
Membuat middleware.ts di root proyek Anda (atau src/middleware.ts jika Anda menggunakan src direktori):
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'],
};
Bagaimana cara kerjanya
Pencocokan rute
Itu config.matcher array memberi tahu Next.js rute mana yang memicu middleware ini.
Ubah jalur ini agar sesuai dengan titik akhir pendaftaran Anda. Middleware berjalan di edge sebelum pengendali rute Anda dijalankan,
jadi permintaan yang ditolak tidak pernah menyentuh database atau penyedia autentikasi Anda.
Ekstraksi email
Middleware membaca isi permintaan dengan req.json() dan menariknya email bidang.
Jika penguraian gagal atau tidak ada email, permintaan tidak akan tersentuh.
Hal ini membuat middleware tidak terlihat oleh rute non-pendaftaran.
Panggilan API
Satu POST ke https://api.botoi.com/v1/disposable-email/check dengan email di badan.
Responnya meliputi:
{
"success": true,
"data": {
"email": "throwaway@mailinator.com",
"domain": "mailinator.com",
"is_disposable": true,
"is_free": false,
"provider": "Mailinator"
}
}
Itu is_disposable bendera adalah gerbangnya. Kapan itu true, middleware mengembalikan 422 dengan pesan yang jelas.
Kapan itu false, NextResponse.next() biarkan permintaan berlanjut ke penangan pendaftaran Anda.
Desain gagal-buka
Itu catch blok di sekitar panggilan pengambilan berarti kegagalan jaringan, waktu tunggu habis, atau waktu henti API tidak mengganggu pendaftaran.
Middleware mencatat peringatan dan mengizinkan permintaan lewat. Pengguna Anda tidak pernah melihat kesalahan yang disebabkan oleh gangguan pihak ketiga.
Menangani kasus tepi
Batas waktu
API botoi merespons dalam waktu kurang dari 50 md untuk sebagian besar permintaan karena menggunakan daftar domain dalam memori.
Jika Anda ingin waktu tunggu yang sulit, selesaikan pengambilan 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
});
Batasan tarif
Tingkat gratis mengizinkan 5 permintaan per menit. Jika aplikasi Anda memproses lebih banyak pendaftaran dari itu, mendapatkan kunci API dan meneruskannya sebagai token Pembawa:
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${process.env.BOTOI_API_KEY}\`,
},
body: JSON.stringify({ email }),
});
Toko BOTOI_API_KEY di dalam kamu .env.local mengajukan. Jangan pernah mengkomitnya ke kontrol versi.
Pemeriksaan duplikat
Jika email yang sama mengenai titik akhir pendaftaran Anda dua kali berturut-turut dengan cepat (klik dua kali, coba lagi logikanya), Anda akan membuat dua panggilan API untuk domain yang sama. Untuk sebagian besar aplikasi, ini baik-baik saja. Jika itu penting, tambahkan cache yang berumur pendek (dibahas di bawah).
Pengerasan produksi
Tambahkan cache dalam memori
Simpan hasil cek sekali pakai per domain selama 5 menit. Hal ini mengurangi panggilan API dan mempercepat pemeriksaan berulang untuk domain yang sama:
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
}
}
Cache berbasis Peta ini berfungsi dalam runtime tanpa server dan edge. Untuk penerapan multi-instance, tukarkan dengan Redis atau 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;
}
}
Daftarkan domain perusahaan yang diizinkan
Beberapa perusahaan menggunakan domain khusus yang tidak ingin Anda blokir, meskipun domain tersebut memiliki pola yang mencurigakan. Tambahkan daftar yang diizinkan:
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
}
Catat upaya yang diblokir
Lacak domain mana yang ditolak sehingga Anda dapat memantau pola penyalahgunaan dan menyesuaikan strategi Anda:
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 lengkap dengan semua pengerasannya
Berikut file lengkap dengan caching, daftar yang diizinkan, batas waktu, dan logging terstruktur:
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'],
};
Mengujinya secara lokal
Mulai server dev Next.js Anda dan aktifkan permintaan dengan email sekali pakai yang diketahui:
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "test@mailinator.com", "password": "hunter2"}'
Respon yang diharapkan:
{
"error": "Disposable email addresses are not allowed. Please use a permanent email."
}
Coba email yang sah untuk mengonfirmasi bahwa email tersebut lolos:
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "dev@acme-corp.com", "password": "hunter2"}'
Permintaan ini mencapai pengendali pendaftaran Anda seperti biasa.
Kapan harus memeriksa klien juga
Middleware menangkap email sekali pakai di server. Namun Anda juga dapat memanggil API yang sama dari formulir pendaftaran Anda untuk menampilkan kesalahan sebaris *sebelum* pengguna mengirimkan. Pemeriksaan cepat di sisi klien setelah bidang email kehilangan fokus menghemat perjalanan pengguna dan memberikan umpan balik yang lebih cepat:
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
}
Middleware masih bertindak sebagai gerbang otoritatif. Pemeriksaan sisi klien adalah peningkatan UX, bukan tindakan keamanan.
FAQ
- Apakah ini berfungsi dengan Router Aplikasi Next.js?
- Ya. Middleware Next.js berjalan sebelum pengendali rute apa pun, terlepas dari apakah Anda menggunakan App Router atau Pages Router. File middleware.ts berada di root proyek (atau di dalam src/ jika Anda menggunakan direktori src), dan file tersebut memotong permintaan ke jalur yang cocok sebelum mencapai rute API atau tindakan server Anda.
- Apakah saya memerlukan kunci API untuk pemeriksaan email sekali pakai botoi?
- Tidak. Tingkat gratis mengizinkan 5 permintaan per menit tanpa kunci API. Untuk aplikasi produksi yang menangani lebih banyak pendaftaran, ambil kunci dari halaman dokumen botoi API untuk membuka batas kapasitas yang lebih tinggi.
- Apa yang terjadi jika API botoi tidak aktif?
- Middleware menangkap kesalahan jaringan dan membiarkan permintaan lewat. Pendekatan gagal-terbuka ini berarti pemadaman API sementara tidak pernah menghalangi pengguna yang sah untuk mendaftar. Anda dapat menambahkan logging untuk melacak kapan perilaku fallback mulai terjadi.
- Bisakah saya menggunakan ini dengan kerangka kerja lain seperti Remix atau SvelteKit?
- Panggilan API itu sendiri berfungsi dari lingkungan sisi server mana pun. Pola middleware yang ditampilkan di sini khusus untuk Next.js, tetapi logika inti (POST ke titik akhir, centang is_disposable di respons) diterjemahkan langsung ke pemuat Remix, kait SvelteKit, atau middleware Express.
- Seberapa akurat deteksi email sekali pakai?
- Titik akhir memeriksa daftar 700+ domain sekali pakai yang dikenal dan menggunakan pencocokan pola untuk menangkap variasi. Ini juga mengidentifikasi penyedia email gratis (Gmail, Outlook, Yahoo) secara terpisah dari penyedia email sekali pakai, sehingga Anda dapat membedakan antara Gmail pribadi dan alamat Mailinator sekali pakai.
Mulai membangun dengan botoi
150+ endpoint API untuk pencarian, pemrosesan teks, pembuatan gambar, dan utilitas developer. Paket gratis, tanpa kartu kredit.