コンテンツへスキップ
Integration

Next.js の使い捨てメールを 1 つのミドルウェア ファイルでブロックする

| 6 min read

botoi API を呼び出して一時的な電子メール アドレスからのサインアップを拒否する 40 行の Next.js ミドルウェア。 コピー、ペースト、デプロイ。

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

ユーザーがサインアップするのは、 test92847@mailinator.com、無料トライアルを焼き尽くして消えます。 彼らは明日、一緒に戻ってきます test92848@mailinator.com そしてもう一度やってください。 サポート キューが架空のアカウントでいっぱいになります。 分析では、何の意味もない水増しされたユーザー数が表示されます。 アカウントがすでにリソースを消費しているため、不正行為の検出が開始されるのが遅すぎます。

修正方法は、サインアップ リクエストがデータベースに到達する前に、使い捨て電子メールを入り口でブロックすることです。 このガイドでは、単一のミドルウェア ファイルを使用し、フェッチ呼び出し以外の依存関係を持たずに Next.js でこれを行う方法を示します。

何を構築するか

サインアップ エンドポイントへの POST リクエストをインターセプトする Next.js ミドルウェア。 リクエスト本文から電子メールを抽出し、それを botoi 使い捨てメール API、 電子メールが使い捨てサービスに属している場合は、422 応答を返します。 ファイル全体は 50 行未満です。

ミドルウェア

作成する middleware.ts プロジェクトのルート (または src/middleware.ts を使用する場合 src ディレクトリ):

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

仕組み

ルートマッチング

config.matcher 配列は、どのルートがこのミドルウェアをトリガーするかを Next.js に伝えます。 サインアップ エンドポイントに一致するようにこれらのパスを変更します。 ミドルウェアはルート ハンドラーが実行される前にエッジで実行されます。 したがって、拒否されたリクエストはデータベースや認証プロバイダーに影響を与えることはありません。

電子メールの抽出

ミドルウェアはリクエスト本文を次のように読み取ります。 req.json() そして引っ張ります email 分野。 解析が失敗した場合、または電子メールが存在しない場合、リクエストはそのまま通過します。 これにより、ミドルウェアはサインアップ以外のルートからは見えなくなります。

API呼び出し

単一の POST で https://api.botoi.com/v1/disposable-email/check 本文にメールを含めます。 応答には次のものが含まれます。

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

is_disposable 旗は門です。 そうなったとき trueの場合、ミドルウェアは明確なメッセージを含む 422 を返します。 そうなったとき falseNextResponse.next() リクエストがサインアップ ハンドラーに継続されるようにします。

フェールオープン設計

catch フェッチ呼び出しの周りのブロックは、ネットワーク障害、タイムアウト、または API ダウンタイムによってサインアップが中断されないことを意味します。 ミドルウェアは警告をログに記録し、リクエストを通過させます。 サードパーティの機能停止によるエラーがユーザーに表示されることはありません。

特殊なケースの処理

タイムアウト

botoi API は、メモリ内のドメイン リストを使用するため、ほとんどのリクエストに対して 50 ミリ秒未満で応答します。 ハードタイムアウトが必要な場合は、フェッチをラップします。 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
});

レート制限

無料枠では 1 分あたり 5 件のリクエストが許可されます。 アプリがそれより多くのサインアップを処理する場合、 APIキーを取得する それを Bearer トークンとして渡します。

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

BOTOI_API_KEY あなたの中で .env.local ファイル。 決してバージョン管理にコミットしないでください。

重複チェック

同じ電子メールがサインアップ エンドポイントに 2 回続けて到達した場合 (ダブルクリック、ロジックの再試行)、 同じドメインに対して 2 つの API 呼び出しを行います。 ほとんどのアプリではこれで問題ありません。 重要な場合は、有効期間の短いキャッシュを追加してください (以下で説明します)。

生産強化

インメモリキャッシュを追加する

破棄可能なチェック結果をドメインごとに 5 分間キャッシュします。 これにより、API 呼び出しが減り、同じドメインの繰り返しチェックが高速化されます。

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
  }
}

このマップベースのキャッシュは、サーバーレスおよびエッジ ランタイムで機能します。 マルチインスタンスのデプロイメントの場合は、Redis または 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;
  }
}

企業ドメインをホワイトリストに登録する

企業によっては、たとえ疑わしいパターンに一致したとしても絶対にブロックしたくないカスタム ドメインを使用していることがあります。 ホワイトリストを追加します。

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
}

ブロックされた試行をログに記録する

どのドメインが拒否されたかを追跡することで、悪用パターンを監視し、戦略を調整できます。

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 }
  );
}

すべての強化を備えた完全なミドルウェア

キャッシュ、許可リスト、タイムアウト、構造化ログを含む完全なファイルは次のとおりです。

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

ローカルでテストする

Next.js 開発サーバーを起動し、既知の使い捨て電子メールでリクエストを発行します。

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

予想される応答:

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

正規の電子メールを試して、通過することを確認します。

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

このリクエストは通常​​どおりサインアップ ハンドラーに届きます。

クライアントの状態もチェックするタイミング

ミドルウェアはサーバー上の使い捨て電子メールをキャッチします。 ただし、サインアップ フォームから同じ API を呼び出すこともできます。 ユーザーが送信する「前」にインライン エラーを表示します。 電子メールフィールドがフォーカスを失った後のクライアント側の簡単なチェック ユーザーの往復時間を節約し、より迅速なフィードバックを提供します。

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
}

ミドルウェアは引き続き権限のあるゲートとして機能します。 クライアント側のチェックは UX の改善であり、セキュリティ対策ではありません。

FAQ

これは Next.js App Router で機能しますか?
はい。 Next.js ミドルウェアは、App Router を使用するか Pages Router を使用するかに関係なく、ルート ハンドラーの前に実行されます。 middleware.ts ファイルはプロジェクト ルート (または src ディレクトリを使用する場合は src/ 内) にあり、一致したパスへのリクエストが API ルートまたはサーバー アクションに到達する前にインターセプトします。
botoi 使い捨てメールチェックには API キーが必要ですか?
いいえ。無料枠では、API キーなしで 1 分あたり 5 つのリクエストが許可されます。 より多くのサインアップを処理する運用アプリの場合は、botoi API ドキュメント ページからキーを取得して、より高いレート制限のロックを解除します。
Botoi API がダウンするとどうなりますか?
ミドルウェアはネットワーク エラーを捕捉し、リクエストを通過させます。 このフェールオープン アプローチは、一時的な API の停止によって正規ユーザーのサインアップが妨げられることがないことを意味します。 フォールバック動作が開始されるタイミングを追跡するためのログを追加できます。
これを Remix や SvelteKit などの他のフレームワークと一緒に使用できますか?
API 呼び出し自体は、任意のサーバー側環境から機能します。 ここで示すミドルウェア パターンは Next.js 固有ですが、コア ロジック (エンドポイントへの POST、応答の is_disposable の確認) は Remix ローダー、SvelteKit フック、または Express ミドルウェアに直接変換されます。
使い捨て電子メールの検出はどの程度正確ですか?
エンドポイントは、700 を超える既知の使い捨てドメインのリストと照合し、パターン マッチングを使用してバリエーションを検出します。 また、無料メール プロバイダー (Gmail、Outlook、Yahoo) を使い捨てのものとは別に識別するため、個人用の Gmail と使い捨ての Mailinator アドレスを区別できます。

botoiで開発を始めよう

150以上のAPIエンドポイント。検索、テキスト処理、画像生成、開発者ユーティリティに対応。無料プラン、クレジットカード不要。