0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Node.js|Resend(リセンド)を導入して、簡単に「メール送信機能」を実装する

Posted at

概要

メール送信機能を簡単に実装できる Resend(リセンド)を導入するための備忘録。
いろんな言語に対応しているが、今回は Node.js (JS環境)で実装する。

メール送信機能を実装できるサービスは色々あるが、有名なのは SendGrid、Amazon SES など。あとは、執筆時点ではまだオープンベータになっていないが、Cloudflare の新サービス。

そんな中でも、Resend は 「とりあえず、サクッとメール送信機能だけ実装したいな〜」 ってなった時に、一番お手軽で精神的にも気楽そうだったので使ってみました 🙌

前提

筆者の開発環境を共有するので、異なる人は適宜読み替えてください🙏

  • マシンOS: macOS
  • 技術スタック:
    • Node.js 22(JS 実行環境として)
    • Hono 4(ローカル環境の動作確認用)
      • @hono/node-server(Hono の Node.js 環境用アダプター)
    • Angular 20 + Firebase
      • Cloud Functions for Firebase を使ったやり方
      • Angular Fire(公式 Firebase ライブラリ、Firebase API ラッパー的なもの)も使用
      • firebase-tools(Firebase 上に環境変数を設定するために利用)
    • Next.js 16(※ 後で実装方法を追記します)
    • Cloudflare Registrar
      • ドメイン購入、独自ドメイン登録時の承認に使用
      • ドメインの購入と設定ができれば Cloudflare 以外のベンダーでも問題ない
  • アプリケーション実装するには、独自ドメイン を持っている必要があることに注意
    • 持っていなければ事前にどこかのドメインレジストラで購入する
    • ローカル環境の簡単なテストだけなら、ドメイン不要(後述:「ローカル環境からメール送信をテストする」に手順を記載)

お安いの?料金プラン

代表的な SendGrid(センドグリッド)と比較してみる 👀
(※ 本記事執筆時点)

結論、Resend の方がやや料金の区分けが細かく、比較的利用しやすい印象(価格以外に機能面・サポート面の違いもあるので注意)

メール送信数/月 Sendgrid Resend Resend(円換算)
100 通/日 ※ 0円 - -
3000 通 3,000円 0ドル 0円
5000 通 3,000円 20ドル 約3,080円
1万2000通 3,000円 20ドル 約3,080円
5万通 3,000円 20ドル 約3,080円
10万通 5,400円/Essential
14,000円/Pro
35ドル/Pro
90ドル/Scale
約5,400円/Pro
約13,800円/Scale
20万通 37,500円 160ドル 約24,500円
30万通 37,500円 350ドル 約53,600円
50万通 75,000円 350ドル 約53,600円
70万通 75,000円 650ドル 約99,500円
100万通 125,000円 650ドル 約99,500円
150万通 125,000円 825ドル 約126,400円
250万通 165,000円 1,050ドル 約160,800円
250万通以上 165,000円+0.071円/通 カスタムプラン -
390万通以上 カスタムプラン カスタムプラン -

※ SendGrid の Free プランの特別計算

SendGrid 料金プラン👇

Redend 料金プラン👇

日本国内のカスタマーサポート という点では、まだ Resend は未対応で、SendGrid の方が代理店も多いので強い印象です。
規模が大きなビジネスだと、そこも重要になるかと思います。

手順

Recend 公式ページにアクセス

右上か左下の「Get Started」からサインアップ開始
resend_01

ソーシャル認証かメールアドレス登録か好きな方式を選ぶ(筆者は Google アカウントで登録した)
resend_02

すぐに登録が完了して、ダッシュボード表示。もう触れる!早い!(メールアドレス直接登録だと、おそらく本人認証ステップがあると思われます)
resend_03

さて、ウェルカムメールの案内によると 「ここからは3ステップ」 だそう。

  1. "Send your first email"
  2. "Add your domain"
  3. "Check the docs"

先に言っておくと、Resend はチュートリアル体験が素晴らしいので是非体験してほしい 🎉

1. とりま、メール1通送れ(Send your first email)

まずは、API キー を発行する。

最初に表示されたチュートリアルページで 「Add API Key」 を押して API キーを発行する。
発行されたキーは コピーアイコンボタン をクリックするとコピーできるので、漏洩に気をつけながら、どこかに値を控えておく。

※ ちなみに、/onboarding ページ(チュートリアル用ページ)の「Add API Key」ボタン以外にも、/api-keys ページの「Create API Key」ボタンを押下してキーを発行することもできる。

次に、"Send an email" という実装コードのサンプルが現れる。
そこに 「Send email」 というボタンがあるので、それをポチッと押す。
すると、ログインアカウントのメールアドレスに すぐメールが届いて動作確認ができる

resend_04

ただし、最初は「受信トレイ」ではなく、「迷惑メール」「プロモーション」 などに受信されている可能性があるので、迷惑メールではないことマークしておく。

resend_05

Resend の素晴らしい UX

通常のライブラリやサービスは、❶ API キー発行 👉 ❷ サンプルに合わせてローカルで実装 👉 ❸ ミスなく実装すれば動作確認ができる、というステップだが、ローカルで動作確認の環境とコードを多少用意する手間がかかる。

しかし、Resed は 「ローカル環境にソースコードを用意しなくても、API キーを発行してテスト送信ボタンを押すだけで、ログインアカウントに紐づけられているメールアドレスですぐに動作確認(送信確認)ができる」 というチュートリアル体験になっている。

つまり、「あっ、発行したキーを使ってサンプルの通りに実装すれば、こうやって動くんだ」 というユーザー体験をすぐに得られるサービス設計にしている。

Resend、やるじゃん!素晴らしいじゃん 🎉

2. 自分のドメインを登録しろ(Add your domain)

実は、この時点でローカル環境からメール送信テストは可能になる(後述:「ローカル環境からメール送信をテストする」)。

しかし、ローカル環境からでは、セキュリティ上簡単な確認しかできないので、アプリケーションへの実装を進めたい場合は、自分が所有する独自ドメインを登録しよう。

resend_06

独自ドメインを登録する。"Name" に独自ドメイン(サブドメインを含めても良い)を設定。リージョン(地域)を設定。
リージョンは、実際にアプリケーションを配置する地域に近い場所を選ぶと良い。

resend_07

ドメインをセットすると、自動的にドメインレジストラを推測してくれる場合があり、ボタンをポチって承認するだけで後続の設定処理がすぐに終わる(筆者は、Cloudflare で購入していたので、Cloudflare のボタンが出てきた)。
出てこない場合は 「Manual setup」 を押下。

resend_08

ちなみに、Cloudflare の自動セットアップは、表示されたポップアップで承認ボタンを押すだけで完了。

resend_09

設定が完了すると、Resend の Domain 画面に遷移して設定処理が走る。

resend_10

👇 数分待つと、無事に承認が完了する。

resend_11

以上で、独自ドメイン登録・承認が完了。
ローカル環境からでも独自ドメインを使用して動作確認ができるようになる(後述:「ローカル環境からメール送信をテストする方法」)。

3. あとは、ドキュメント読め(Check the docs)

設定に関しては、既に完了しているので、あとは実装するのみ。
初心者は、ドキュメントページ を見て知らないことを調べてみると良い。

制約

当然と言われれば当然だが、セキュリティ上、動作確認するにも以下の制約がある(特に、ローカル環境で動作確認したい場合)。

  • そもそもフロントエンドから直接は送れない(サーバサイド処理として送信する仕組みの導入が必要)
  • resend.com/domains で認証済みのドメインからしか、メールは送信されない(=「ドメイン登録」をしていないドメインからは送れない)
  • Resend のドメインを from に指定する場合(@resend.dev)、Resend に登録された 自分のメールアドレス宛 にしか送信できない(= 他のメールアドレスへの送信テストはできない)

アプリケーションに実装する(Angular)

では、実際にアプリにメール送信機能を実装してみる。

「制約」にある通り、クライアントサイドから直接送信はできず サーバサイドが必要 なので、今回は、Firebase の Functions を使って「メール送信処理」を関数化し、サーバレスに構築するパターンを紹介(Supabase などでも同様になると思います)。

尚、Firebase の初期設定や導入方法、firebase-tools, Angular Fire のインストール方法等は、今回解説しない。

Secret Manager に API キーを登録

Firebase Functions の Secret Manager に環境変数として Recend の API キーを登録する。

あらかじめ、npm で firebase-tools をグローバルインストールし、firebase login で利用予定のプロジェクトにログインしておく。

# 変数名 "RECEND_API_KEY" としてセットする
$ firebase functions:secrets:set RECEND_API_KEY

# 以下のように値を求められるので、コピペしたキーを貼付(貼付しても表示はされない)してEnter キー
? Enter a value for MY_SECRET [input is hidden]

Functions 実装

Functions のプロジェクトで Recend のパッケージをインストール

npm i recend

関数を実装して、Functions にデプロイ
※ 以下はあくまで一例です。実際の用途に必要な書き方に修正してください。

functions/src/send-email.ts
import {
  onCall,
  HttpsError,
} from "firebase-functions/v2/https";
import {Resend} from "resend";

/** メール送信 (リクエスト) 型定義 */
interface SendEmailRequest {
  to: string[];
  subject: string;
  content: string;
}

/** メール送信 (レスポンス) 型定義 */
interface SendEmailResponse {
  success: boolean;
  emailId?: string;
  error?: string;
}

/** メールを送信 */
export const sendEmail = onCall(
  {
    region: "asia-northeast1", // Firebase のリージョンコード指定
    secrets: ["RESEND_API_KEY"],
  },
  async (request) => {
    // 用意した型定義を使ってリクエスト/レスポンスをセットする
    const response = {success: false} as SendEmailResponse;
    const {to, subject, content} = request.data as SendEmailRequest;

    // 認証チェック
    if (!request.auth) {
      throw new HttpsError(
        "unauthenticated",
        "User must be authenticated."
      );
    }

    // API キーチェック
    const apiKey = process.env.RESEND_API_KEY;
    if (!apiKey) {
      throw new HttpsError(
        "failed-precondition",
        "RESEND_API_KEY is not configured."
      );
    }

    // リクエストデータのバリデーション
    if (!to || !Array.isArray(to) || to.length === 0) {
      throw new HttpsError(
        "invalid-argument",
        // eslint-disable-next-line quotes ※シングルクォートが Lint エラーになったので回避
        'request "to" must be a non-empty array of email addresses.'
      );
    }

    try {
      // メール送信
      const resend = new Resend(apiKey);
      const {data, error} = await resend.emails.send({
        from: "差出人名 <no-reply@example.com>", // 承認された独自ドメインを指定
        to,
        cc: [] as string[], // 任意で CC も設定可能
        subject: subject, // 件名
        html: content, // 本文:HTML 文字列をセット
      });

      // Recend のエラーハンドリング
      if (error) {
        console.error("[sendEmail] Resend error:", error);
        return {
          success: false,
          error: error.message || "Failed to send email",
        };
      }

      // 送信成功時レスポンス
      return {
        success: true,
        emailId: data.id,
      };
    } catch (error) {
      console.error("[sendEmailInviteUser] Unexpected error:", error);
      throw new HttpsError(
        "internal",
        "Failed to send email due to unexpected error."
      );
    }
  }
);

Angular 実装

メール送信サービスとして Service クラスを用意

import { Injectable, inject, Injector, runInInjectionContext } from '@angular/core';
// AngularFire のクラスを利用
import { FirebaseError } from '@angular/fire/app';
import { Functions, httpsCallable } from '@angular/fire/functions';

@Injectable({
  providedIn: 'root'
})
export class SendEmailService {
  private _injector = inject(Injector);
  private _functions = inject(Functions);

  /** ユーザー招待メール送信 */
  async sendEmailUserInvitation(sendTo: string[], subject: string, content: string) {

    // ログインユーザ情報取得
    const user = this._auth.authedUser;
    const organization = this._auth.organization;
    if (!user) {
      await this._auth.warnNoUserDataAndLogout();
      return;
    }

    try {
      // 何か型定義しておくと良い(interface など)
      const request = {
        to: sendTo,
        subject,
        content,
      }

      // 関数実行: Functions に設定した関数名をコールする
      const callable = runInInjectionContext(
        this._injector,
        () => httpsCallable(this._functions, 'sendEmail')
      );
      const result = await callable(request);

      // 成功時処理を何か書く
      const response = result.data; // 成功時の ID が取れる

    } catch (error) {
      // エラーハンドリングを何かする
      if (error instanceof FirebaseError) {
        console.error('[FirebaseError]: ', error);
      } else {
        console.error('[Error]: ', error);
      }

    }
  }

}

あとは、コンポーネントでこのサービスを実行するボタンやトリガーを用意すれば、メールが送信される。

アプリケーションに実装する(Next.js)

Next.js の場合は、Angular と違ってサーバサイド処理を簡単に実装できるため、フレームワーク内で実装が完結できる。

(現在執筆中…✍️)

ローカル環境から簡単にメール送信をテストする方法

自分た実装する予定のアプリケーションに関係なく、「とりあえずテストしたい」という人向け。

とりあえず API キーだけあれば、以下の手順でローカル環境での簡単な動作確認はできる

使うツール・サービス

ローカル環境の動作確認では、以下の2つを使う。

  • Hono(JS ランタイム用フレームワーク)
    • メール送信 API 処理を実装し、実際に送信するために使用
  • Talend API Tester(Chrome 拡張機能)
    • 実装した API をブラウザから呼び出すために使用
    • Postman 等でも可だが、導入・使用が直感的で簡単なので採用

Hono

Talend API Tester

実装サンプル

Hono のプロジェクトを用意(ワンライナー実行コマンド)

npm create hono@latest demo-app -- --template nodejs --pm npm --install 

以下の設定でプロジェクトが生成される

  • アプリ名: demo-app
  • ランタイム: Node.js
  • パッケージマネージャ: npm
  • 依存関係インストール: する

Resend(+ dotenv)をインストール

npm i resend dotenv

プロジェクトのルート(直下)に .env を作成して、Resend の API キーをセット

RESEND_API_KEY=re_xxxxx # API キー貼付

以下のように /send など任意のエンドポイントで処理を実装。テスト用なので GET メソッドとかで良い。

src/index.ts
import 'dotenv/config'
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { Resend } from 'resend'

const app = new Hono()
const resend = new Resend(process.env.RESEND_API_KEY!)

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

// エンドポイントを作成
app.get('/send', async (c) => {
  try {
    const { data, error } = await resend.emails.send({
      from: '私だ😇 <onboarding@resend.dev>',
      to: 'xxx@example.com', // Resend に登録したメールアドレス
      subject: 'テスト送信😎',
      html: '<p><strong>おっす</strong>🎉 Email 送ってみたぞ!</p>',
    });

    // エラー時 HTTP 400 返却
    if (error) return c.json({ error }, 400)

    // 正常レスポンス
    return c.json({ data })
  } catch (e: any) {
    // 例外時 HTTP 500 返却
    return c.json({ error: e?.message ?? 'unknown error' }, 500)
  }
})

serve({
  fetch: app.fetch,
  port: 3000
}, (info) => {
  console.log(`Server is running on http://localhost:${info.port}`)
})

「ドメイン認証」の工程を完了すれば、from に独自ドメインのメールアドレスを指定でき、to にも自分以外のメールアドレスを指定してテストできるようになる。

実際に API を叩いて送信テスト

Talend API Tester(導入方法などは割愛)で、今回用意した API のエンドポイントを呼んでみる。

http:localhost:3000/send を GET 指定して Send ボタンを押下すると、無事に動いた。

xxxxxxx

無事、実際メールも飛んできた

xxxxxxx

制約を守らないと発生するエラー

前述の制約により、準拠しないと以下のエラーが発生する設計になっている。

ローカル環境のフロントエンドから送信すると発生するエラー

以下のように CORS が原因でエラーとなる。解決するには、サーバサイドで実装する。これは、公式にも言及されている1

<送信処理実行箇所> Access to fetch at 'https://api.resend.com/emails' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

オリジン 'http://localhost:4200' から 'https://api.resend.com/emails' へのフェッチへのアクセスが CORS ポリシーによってブロックされました: プリフライト要求への応答がアクセス制御チェックに合格しません: 要求されたリソースに 'Access-Control-Allow-Origin' ヘッダーが存在しません。

Resend が未認証のドメインのメールを指定すると発生するエラー

"from" に Resend 認証していないドメインのメールアドレスを指定すると、以下のエラーメッセージと共にエラーが発生する。

▶︎ エラーメッセージ
You can only send testing emails to your own email address (<登録したメールアドレス>). To send emails to other recipients, please verify a domain at resend.com/domains, and change the from address to an email using this domain.

「テストメールはご自身のメールアドレスにのみ送信できます。他の受信者にメールを送信するには、resend.com/domains でドメインを検証し、「送信元」アドレスをこのドメインを使用したメールに変更してください。」

まとめ

本記事は以上です。

終わってみれば、手順を整理すると「簡単」なのですが、何もわからない中でいきなり導入しようとすると意外とハマりどころがあります。
個人用(自分用)に手順を整理して、ローカルに簡単なメモとして残しておくと良いかもしれません。

今回は Hono や Angular を題材に採用しましたが、JS/TS 系のフレームワークであれば基本的に読み替えて利用できる内容かと思います。

メール実装機能を実装したい、どなたかの参考になれば幸いです🙌

  1. Resend|How do I fix CORS issues"

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?