💁‍♂️

Node.js / Express / TypeScript / MySQLでメール送信機能の設計と実装について

2023/08/20に公開

はじめに

バックエンドからメールを送信する機能が含まれるシステムは一般的であると思われます。送信者全てに対して、同じ内容を送るメールもあれば、タイトルや本文の一部分を動的に変更して、メールを送るユースケースが多いでしょう。

上述の内容を考慮した上でのテーブル設計とメール送信処理についての記事を執筆しました。

背景としては、約3年前にNode+Sequelize+AWS SESでメール一括送信処理の設計と実装をした経験があります。その際に行ったテーブル設計は、今でも他のシステム開発の際に活用できており、今回のタイミングで整理してみることにしました。

環境

  • Node v18.14.0
  • Express v4.18.2
  • TypeScript v5.1.3
  • TypeORM v0.3.16
  • MySQL 8.0
  • InversifyJS v6.0.1

論理ER設計及び初期データ投入

DB設計

ER図

テーブルの設計方針としては、以下の3つを前提としております。

  • テーブルにはサロゲートキーとしてid列を持たせる(id列は一意の値を付与する)
  • 中間テーブルには、サロゲートキーを付与しない
  • 本ERでは省略するが、テーブルには、更新日時と作成日時列を持たせる

email_templatesテーブル

  • コード

    • プログラムからメールテンプレートを取得する際に特定するための一意なコード値を割り当てる
    • サロゲートキーでも良いですが、シーケンシャルなため、オペレーションによって値が環境によっては異なる蓋然性があることからコード列を設けています
  • テンプレート名称

    • テーブルを参照した際に、何のテンプレートであるかがすぐ分かるように名称を管理できる列を設けている
  • タイトル

    • メールの件名
  • 本文

    • メールの本文
    • 動的にマッピングしたい値には、{{userName}}のようにプレースホルダー形式で定義します

email_tagsテーブル

  • タグ
    • プレースホルダーとして、設定するタグを登録する

email_template_tagsテーブル

  • email_templatesテーブルとemail_tagsテーブルの中間テーブル
  • タグは複数のメールテンプレート内でも使用されることが想定されるため、より汎用的に利用できるように中間テーブルで管理している

DDL / DML

CREATE TABLE email_templates (
    id BIGINT NOT NULL AUTO_INCREMENT,
    code VARCHAR(10) NOT NULL UNIQUE,
    template_name VARCHAR(100) NOT NULL,
    title TEXT NOT NULL,
    body TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);

INSERT INTO email_templates (code, template_name, title, body) VALUES
('EMAIL_001', 'Daily Report Notification', 'デイリーレポートを作成しました', '{{userName}} さん\n\n道場生がデイリーレポートを作成しました!!\nご確認とレビューお願いします。\n\n{{reportUrl}}\n\nよろしくお願いします。');

CREATE TABLE email_tags (
    id BIGINT NOT NULL AUTO_INCREMENT,
    tag_name VARCHAR(50) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);

INSERT INTO email_tags (tag_name) VALUES
('userName'),
('reportUrl');

CREATE TABLE email_template_tags (
    email_templates_id BIGINT NOT NULL,
    email_tags_id BIGINT NOT NULL,
    FOREIGN KEY (email_templates_id) REFERENCES email_templates(id),
    FOREIGN KEY (email_tags_id) REFERENCES email_tags(id),
    PRIMARY KEY (email_templates_id, email_tags_id)
);

INSERT INTO email_template_tags (email_templates_id, email_tags_id) VALUES
(1, 1), -- userNameタグ
(1, 2); -- reportUrlタグ

実装

前回執筆したNode.js / Express / TypeScript / たぶんDDDでスケルトンのAPIを作ったのバックエンドに活用方法に特化したコードの例を示したいと思います。

メールテンプレートのプレースホルダーを置換する共通メソッド

backend/src/utils/email/replacePlaceholders.ts
/**
 * メールテンプレートのプレースホルダーを置換する
 * @param emailTemplate 置換するプレースホルダーを含むメールテンプレート
 * @param placeholders プレースホルダーに対する置換文字列
 * @returns 置換後のメールテンプレート
 */
export const replacePlaceholders = (
  emailTemplate: string,
  placeholders: Record<string, string>
): string => {
  // 二重波括弧 {{}} で囲まれた文字列をプレースホルダーとして扱う
  return emailTemplate.replace(/\{\{(\w+)\}\}/g, (_, key) => {
    return placeholders[key] || `{${key}}`;
  });
};

利用サンプルコード

    // EMAILテンプレートを取得する
    const code = "EMAIL_001";
    const emailTemplatesEntity =
      await this._emailTemplatesRepository.fetchEmailTemplatesByCode(code);
    console.log("emailTemplatesEntity: ", JSON.stringify(emailTemplatesEntity));
    // プレースホルダーに対して値を設定する
    const placeholders = this.buildPlaceholders(
      emailTemplatesEntity.templateTags
    );
    const emailBody = replacePlaceholders(
      emailTemplatesEntity.body,
      placeholders
    );
    console.log("emailBody: ", emailBody);

プレースホルダーを構築するプライベートメソッド

  private buildPlaceholders(
    emailTags: EmailTemplateTagsEntity[]
  ): Record<string, string> {
    return emailTags.reduce((obj, tag) => {
      const tagName = tag.emailTag.tagName;

      if (tagName === "userName") {
        obj[tagName] = "村田 権三郎";
      } else if (tagName === "reportUrl") {
        obj[tagName] = "http://localhost:3000/daily-report/1";
      }

      return obj;
    }, {});
  }

解説

console.log("emailTemplatesEntity: ", JSON.stringify(emailTemplatesEntity));

{
  "id": 1,
  "code": "EMAIL_001",
  "templateName": "Daily Report Notification",
  "title": "デイリーレポートを作成しました",
  "body": "{{userName}} さん\n\n道場生がデイリーレポートを作成しました!!\nご確認とレビューお願いします。\n\n{{reportUrl}}\n\nよろしくお願いします。",
  "createdAt": "2023-08-20T07:12:32.000Z",
  "updatedAt": null,
  "templateTags": [
    {
      "emailTemplatesId": 1,
      "emailTagsId": 1,
      "emailTag": {
        "id": 1,
        "tagName": "userName",
        "createdAt": "2023-08-20T07:12:32.000Z",
        "updatedAt": null
      }
    },
    {
      "emailTemplatesId": 1,
      "emailTagsId": 2,
      "emailTag": {
        "id": 2,
        "tagName": "reportUrl",
        "createdAt": "2023-08-20T07:12:32.000Z",
        "updatedAt": null
      }
    }
  ]
}

email_templatesテーブルとリレーションが張られているテーブルのレコード情報が取得できています。

console.log("emailBody: ", emailBody);

emailBody:  村田 権三郎 さん

道場生がデイリーレポートを作成しました!!
ご確認とレビューお願いします。

http://localhost:3000/daily-report/1

プレースホルダーに対して、値がマッピングされました。

メールを送信前にタイトル、本文が確定したら、それをメール送信メソッドに渡してメールを送信する流れができます。
ユースケースによっては、その場でメールを配信せずに一括で送信するといった場合には、メールキューテーブルを定義して、キューに入れておいて一括送信する機構を作れば良いかと思います。

おわりに

はじめにも書きましたが、私はこちらの設計方針を軸に今回はNode/TSの組み合わせですが、Java、KotlinでAPIを実装した際にも取り入れています。

以上です。
本記事が何かの一助になれば幸いです。

Discussion