resend について
resendは、Resend Email APIをローカルで状態付きエミュレーションするツールです。メールの送信処理をテストする開発者向けで、実際のメールは送らず、送信内容を受信箱UIやREST APIから確認できます。
主な機能
- 01送信メールをメモリに保存
- 02受信箱UIまたはRESTでメールを確認
- 03ドメイン、APIキー、オーディエンス、連絡先をエミュレート
- 04冪等な送信リクエストに対応
- 05宛先許可リストと送信試行をテスト
活用シーン
- 01実メールを送らずにメール配信フローをテスト
- 02テストで認証コードやマジックリンクを抽出
- 03宛先拒否と再試行の挙動を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill resendインストール後、Claude Code で/resendを入力して読み込まれたか確認します。
詳細
Resend Email API エミュレーター
Resend API を完全にステートフルにエミュレートします。メール、ドメイン、API キー、オーディエンス、連絡先はメモリに保存されます。送信されたメールは保存され、受信トレイ UI または REST API から確認できます。
実際のメールは送信されません。受け付けられた POST /emails の呼び出しはメッセージをローカルに保存するため、プログラムから、またはブラウザーで確認できます。検証済みの送信では、受信者による拒否を含め、送信試行も記録されます。
起動
ローカルリスナーはデフォルトで 127.0.0.1 にバインドされます。コンテナーやほかのマシンからアクセスするには、CLI に --host 0.0.0.0 を渡すか、createEmulator に hostname: '0.0.0.0' を指定します。それらのクライアントからアクセスできる URL を通知するには、--base-url または baseUrl を設定します。
# Resend only
npx emulate --service resend
# Default port (when run alone)
# http://localhost:4000
または、プログラムから実行します。
import { createEmulator } from 'emulate'
const resend = await createEmulator({ service: 'resend', port: 4000 })
// resend.url === 'http://localhost:4000'
認証
トークンは Authorization: Bearer <token> として渡します。re_ で始まるトークンはすべて受け付けられます。
curl http://localhost:4000/emails \
-H "Authorization: Bearer re_test_key"
トークンが指定されていない場合、リクエストはデフォルトユーザーにフォールバックします。
アプリからエミュレーターを指定する
環境変数(Resend SDK)
公式の Resend Node.js SDK は、モジュールの読み込み時に RESEND_BASE_URL を読み取ります。これをエミュレーターの URL に設定すると、コードを変更せずに SDK を使用できます。
RESEND_BASE_URL=http://localhost:4000
import { Resend } from 'resend'
// No baseUrl argument needed; the SDK reads RESEND_BASE_URL automatically.
const resend = new Resend('re_test_key')
await resend.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Hello',
html: '<p>It works!</p>',
})
Next.js に組み込む(adapter-next)
@emulators/adapter-next を使用すると、エミュレーターは Next.js アプリ内の /emulate/resend で実行されます。next.config.ts で RESEND_BASE_URL を設定します。
// next.config.ts
import { withEmulate } from '@emulators/adapter-next'
export default withEmulate({
env: {
RESEND_BASE_URL: `http://localhost:${process.env.PORT ?? '3000'}/emulate/resend`,
},
})
// app/emulate/[...path]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
import * as resend from '@emulators/resend'
export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
services: {
resend: {
emulator: resend,
seed: {
domains: [{ name: 'example.com' }],
},
},
},
})
fetch を直接使用する
SDK や環境変数を使用できない場合は、エミュレーターを直接呼び出します。
await fetch('http://localhost:4000/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer re_test_key',
},
body: JSON.stringify({
from: '[email protected]',
to: '[email protected]',
subject: 'Hello',
html: '<p>It works!</p>',
}),
})
シード設定
resend:
domains:
- name: example.com
region: us-east-1
contacts:
- email: [email protected]
first_name: Test
last_name: User
audience: Default
送信済みメールの取得
エミュレーターの大きな特長は、POST /emails で送信されたすべてのメールが保存され、クエリ可能なことです。
受信トレイ UI
ブラウザーで送信済みメールを確認します。
http://localhost:4000/inbox
REST API
# List all sent emails
curl http://localhost:4000/emails \
-H "Authorization: Bearer re_test_key"
# Get a single email by ID
curl http://localhost:4000/emails/<id> \
-H "Authorization: Bearer re_test_key"
メールからのデータ抽出(テスト、エージェント)
マジックリンク、確認コード、パスワードリセットのワークフローをプログラムから完了する場合に便利です。
# Get the latest email ID
EMAIL_ID=$(curl -s http://localhost:4000/emails \
-H "Authorization: Bearer re_test_key" | jq -r '.data[0].id')
# Extract a 6-digit code from the HTML body
CODE=$(curl -s http://localhost:4000/emails/$EMAIL_ID \
-H "Authorization: Bearer re_test_key" | jq -r '.html' | grep -oE '[0-9]{6}')
echo "Verification code: $CODE"
API エンドポイント
メール
# Send an email
curl -X POST http://localhost:4000/emails \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"from": "[email protected]", "to": "[email protected]", "subject": "Hello", "html": "<p>Hi</p>"}'
# Send batch (up to 100)
curl -X POST http://localhost:4000/emails/batch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '[{"from": "[email protected]", "to": "[email protected]", "subject": "One", "html": "<p>1</p>"}]'
# List all emails
curl http://localhost:4000/emails \
-H "Authorization: Bearer $TOKEN"
# Get email by ID
curl http://localhost:4000/emails/<id> \
-H "Authorization: Bearer $TOKEN"
# Cancel a scheduled email
curl -X POST http://localhost:4000/emails/<id>/cancel \
-H "Authorization: Bearer $TOKEN"
サポートされるフィールド:from、to、subject、html、text、cc、bcc、reply_to、headers、tags、scheduled_at。
冪等な送信
POST /emails と POST /emails/batch は、大文字と小文字を区別しない Idempotency-Key ヘッダーを受け付けます。キーは 1~256 文字で、24 時間有効です。同じキーと検証済みのペイロードで再試行すると、メールを作成したり、重複する email.sent および email.delivered ウェブフックを送信したりせずに、元のレスポンス、メール ID、ステータスを返します。異なるペイロードまたはエンドポイントでキーを再利用すると 409 invalid_idempotent_request が返され、キーの長さが無効な場合は 400 invalid_idempotency_key が返されます。ヘッダーを省略した場合は、通常どおり動作します。
curl -X POST http://localhost:4000/emails \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-email-123" \
-d '{"from": "[email protected]", "to": "[email protected]", "subject": "Hello"}'
宛先の制限と送信試行
シード設定で resend.allowlist を設定して、宛先の拒否をテストします。
resend:
allowlist:
- [email protected]
- [email protected]
allowlist を省略するとすべての宛先が許可され、allowlist: [] にするとすべての宛先が拒否されます。to、cc、bcc の各アドレスがすべて登録されている必要があります。照合時には前後の空白と大文字・小文字が無視されます。登録できるのは個々のアドレスのみで、ワイルドカードやドメインによる照合には対応していません。これは Resend のドメイン認証やアカウント制限とは独立した、ローカルテスト用の制御です。
送信が拒否されると、拒否されたアドレスと設定済みの許可リストを示す 403 validation_error が返されます。メール ID、メールレコード、冪等性レコード、Webhook は作成されません。宛先のいずれかが許可されていない場合、バッチ全体が拒否されます。同じ冪等性キーを使って再試行した場合も、拒否されたリクエストは新たな試行として扱われます。受理された冪等リクエストを再生した場合は、その後に許可リストが変更されていても、元のレスポンスが返されます。
検証済みの送信では、拒否されたバッチ内のメールも含め、メールごとに1件のレコードが resend.send_attempts ストアコレクションに追加されます。各レコードには、from、to、cc、bcc、subject、idempotency_key(または null)、およびストアの id、created_at、updated_at が含まれます。メッセージ本文は試行レコードにコピーされません。不正なリクエスト、無効または競合する冪等性キー、冪等性リクエストの再生では、試行レコードは追加されません。
@emulators/resend の getResendStore(store).sendAttempts.all() で試行を読み取るか、永続化された状態を確認してください。共有アダプターのスナップショットでは、キー resend:resend.send_attempts が使用されます。試行と宛先制限は、拒否された送信も含め、保存済み状態の復元後も維持されます。試行を取得するための Resend API エンドポイントはありません。拒否された送信にはメールを伴わない試行レコードが残ります。送信自体が行われなかった場合は、どちらも残りません。
ドメイン
# Create domain
curl -X POST http://localhost:4000/domains \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "example.com", "region": "us-east-1"}'
# List domains
curl http://localhost:4000/domains \
-H "Authorization: Bearer $TOKEN"
# Get domain
curl http://localhost:4000/domains/<id> \
-H "Authorization: Bearer $TOKEN"
# Verify domain (instantly marks all records as verified)
curl -X POST http://localhost:4000/domains/<id>/verify \
-H "Authorization: Bearer $TOKEN"
# Delete domain
curl -X DELETE http://localhost:4000/domains/<id> \
-H "Authorization: Bearer $TOKEN"
API キー
# Create API key
curl -X POST http://localhost:4000/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Production"}'
# List API keys
curl http://localhost:4000/api-keys \
-H "Authorization: Bearer $TOKEN"
# Delete API key
curl -X DELETE http://localhost:4000/api-keys/<id> \
-H "Authorization: Bearer $TOKEN"
オーディエンス
# Create audience
curl -X POST http://localhost:4000/audiences \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Newsletter"}'
# List audiences
curl http://localhost:4000/audiences \
-H "Authorization: Bearer $TOKEN"
# Delete audience
curl -X DELETE http://localhost:4000/audiences/<id> \
-H "Authorization: Bearer $TOKEN"
コンタクト
# Create contact in an audience
curl -X POST http://localhost:4000/audiences/<audience_id>/contacts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "first_name": "Jane", "last_name": "Doe"}'
# List contacts in an audience
curl http://localhost:4000/audiences/<audience_id>/contacts \
-H "Authorization: Bearer $TOKEN"
# Delete contact
curl -X DELETE http://localhost:4000/audiences/<audience_id>/contacts/<id> \
-H "Authorization: Bearer $TOKEN"
Webhook
エミュレーターは、状態が変更されると Webhook イベントを送信します。
POST /emailsでemail.sentとemail.delivered- ドメイン操作で
domain.createdとdomain.deleted - コンタクト操作で
contact.createdとcontact.deleted
よく使われるパターン
マジックリンク/確認コードのフロー
TOKEN="re_test_key"
BASE="http://localhost:4000"
# 1. Send verification email
curl -X POST $BASE/emails \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"from": "[email protected]", "to": "[email protected]", "subject": "Your code", "html": "<p>Code: <strong>482910</strong></p>"}'
# 2. Retrieve the email
EMAIL_ID=$(curl -s $BASE/emails -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')
# 3. Read the HTML body
curl -s $BASE/emails/$EMAIL_ID -H "Authorization: Bearer $TOKEN" | jq -r '.html'
テストでの送信と確認
import { createEmulator } from 'emulate'
import { Resend } from 'resend'
const emu = await createEmulator({ service: 'resend', port: 4000 })
process.env.RESEND_BASE_URL = emu.url
const resend = new Resend('re_test_key')
// Send
await resend.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Verify',
html: '<p>Code: <strong>123456</strong></p>',
})
// Retrieve
const res = await fetch(`${emu.url}/emails`, {
headers: { Authorization: 'Bearer re_test_key' },
})
const { data: emails } = await res.json()
console.log(emails[0].html) // contains "123456"
組み込みエミュレーターと並行して使用するカスタムエミュレーター
npx emulate init --custom inventory を使用して、サードパーティ API エミュレーターとテストをひな形から作成します。emulate の defineConfig を使って、emulate.config.ts に { emulator: "resend" } などの組み込みエントリーと並べて登録します。npx emulate start --watch を実行すると、インポートが再読み込みされ、表示された /_emulate URL でカスタム状態を確認できます。再読み込みが成功すると、実行状態はシードにリセットされます。既存のフラット形式のシード設定も引き続き使用できます。--config で明示的なファイルを指定します。サードパーティ API エミュレーターの作成とテストについては、https://emulate.dev/docs/custom-emulators を参照してください。