stin's Blog

resend-local というツールを作った


Resend というメール送信サービスがあります。 そのAPIエミュレーターをローカルで起動する npm パッケージを作りました。次のコマンドでサーバー http://localhost:8005 で起動します。

# npm
npx resend-local
# pnpm
pnpm dlx resend-local

Resend SDK は Base URL を環境変数経由で変更できます。.envなどで次のように指定すれば、エミュレーターに SDK からリクエストが飛びます。

RESEND_BASE_URL="http://localhost:8005"

Node.js SDK、Rust SDK で環境変数の Base URL を参照することを確認済みです(他のプログラミング言語用 SDK もそうなっているのではと思います)。

http://localhost:8005 をブラウザで Web ページとして開けば、SDK から送信した結果が見れます。現在サポートしているのは次のリソースです

  • Emails
  • Batch Emails
  • Domains
  • API Keys

API Keys は保存していますがリクエスト検証に使っているわけではありません。あくまでインターフェイスのエミュレートですので。

作った理由

Resend を使う機会ができたのですが、ローカル開発中は実際のメール送信をしたくないと思っていました。依存の逆転で依存注入でなんじゃらほいすれば、開発中はメールを送信せずに console.log に流すだけの実装に差し替えられますが、どうせなら送信したメールをいい感じに表示したいなと思いました。

Resend ならローカルエミュレーターくらい提供してるやろと思ったんですけど、ありませんでした。なければ作ればいいじゃない。エンジニアだもの。

AWS SES には aws-ses-v2-local という非公式エミュレーターがあります。これと同じ体験を Resend でもできるようにしたかったのです。

作り方

Next.js standalone モード

ベースのフレームワークは Next.js を使っています。

Next.js には standalone モードというビルドモードがあります。

ソースコードや node_modules 内の JS コードを可能な限りバンドルしてしまい、バイナリなどの関係でバンドルできない node_modules 内の依存を抽出して別ディレクトリに括りだしてくれるビルドモードです。これにより、package.json に書いてあるすべての依存を node_modules に入れた状態で稼働させるよりも少なく小さいファイル数で、実行可能な JS を生成できます。Docker コンテナで Next.js を使うときに、イメージサイズを小さくするために使用するビルドモードですね。

resend-local では、standalone モードビルドによる成果物を npm レジストリに公開することで、動くサーバーコードを配布しています。

npm に公開されているパッケージをインストールするとき、そのパッケージの package.json の dependencies に列挙されている依存パッケージもすべてインストールされてしまいます。standalone モードではほとんどの node_modules はバンドルされているため、ユーザーの node_modules に含まれる必要はありません。なので、多くの依存は dependencies ではなく devDependencies に列挙しました。こちらに書いてある依存なら、resend-local をインストールしても降ってくることはありません。

ただし一部のパッケージは node_modules に残す必要があります。具体的には next.config.ts の serverExternalPackages に列挙されているモジュールたちです。自分で指定しなくても、ビルトインで指定済みのパッケージ名があります。

ここにあるものは dependencies に残しておきました。

これによって最小限のパッケージサイズにしています(vite と適当なサーバーフレームワークにしておけばもっと小さくできると思うけどね。ワシは Next.js が好きなんや)。

API エミュレーター

Resend が OpenAPI spec を公開していました。

これに従って裏側を適当に書けばエミュレーターになると考えました。

OpenAPI からソースコードにするのは、以前 Zenn で見かけた hono-takibi を使いました。

これを使って @hono/zod-openapicreateRouteまではほぼ自動生成でいけました(Resend OpenAPI がやや雑で手直しは必要でした)。

Hono のインスタンスを Next.js の Route Handler にアタッチして使っています。

データベース

データを永続化しておきたかったので、データベースを使うことにしました。SQLite の物理ファイルを npm 配布物に含めています。

データベースクライアントには Drizzle ORM を使っています。SQL に近くて書き心地が良いので好きです。JOIN 書くの面倒で結局 query シンタックス使うんですけど。

パッケージのビルド時に drizzle-kit push を実行して resend-local.sqlite というファイルを生成します。これを配布物ディレクトリに保存しておきます。

UI デザイン

UI ライブラリ(語弊)として Shadcn UI を使っています。

コピペ CLI という提供方法は実はそんなに好きではないのですが、なんとなくクールな UI を TailwindCSS ベースで作れるからという理由で選んでいます。本当は普通の UI ライブラリを使いたい。

Text メールの表示確認のときはリンクらしき文字列を a 要素で表示します。便利じゃない?HTML メールはレンダリングして表示したり、シンタックスハイライトされた HTML 文字列として表示したりできます。どう?便利じゃない?

まとめ

Resend API をローカルでエミュレートする resend-local というツールを作りました。

よかったら使ってみてください。