Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Node.JS + SendGrid + TypeScript

(Almost) Every application needs to send an email at some point. I’ll walk through using ZURB’s Foundation for Email to send great looking emails with the SendGrid API.

Source Code

You can download the source code and follow along or fork the repository on GitHub:

Getting Started

We’ll be using TypeScript (TS) - a superset of JavaScript that includes strong typing and great tooling. We are also going to be using Node.js along with the node package manager (npm).

You will need to install Node, which comes with npm. Then you can install TypeScript:

$ sudo npm install -g typescript

Next, install the sendgrid helper library (or SDK if you will) for your project. If you have not create a project you will need to run npm init to get one started.

$ npm install sendgrid --save

Note that I append the --save flag so that the dependency is saved in our package.json file.

Goals

Before I dive in, here are the goals that I have:

  1. Write a service layer to easily send HTML emails using the ZURB Foundation for Emails 2 project (formerly called Ink).
  2. Use the Mail helper provided by the sendgrid library.
  3. The exposed API should be small and easy to use. I want to send a sweet email in a couple of lines of code with little ceremony.

Foundation for Emails 2

I am going to use Foundation for Emails 2 provided by ZURB. It provides a slick way to create email templates that will work in most major email clients. In other words, it takes the pain out of formatting nice looking email templates.

I’ll be using the Sass version.

First, let’s install the foundation cli:

$ sudo npm install -g foundation-cli

Next, create a new Foundation for Emails project:

$ foundation new --framework emails

Next, start up the server via:

$ cd {PROJECT}
$ npm start

After starting the service your browser should open to http://localhost:3000. The index lists all of the pre-built email templates that come with Foundations for Email. I suggest you pick one that you like, customize it, and remove the rest.

Here is a screen shot of the template that I am using:

Email template screen shot

Note that I have some substitutions defined in my template:

These are placeholders where the sendgrid Mail helper will substitute in values that we specify.

Finally, let’s build our production-ready templates:

$ npm run build

App Structure

Here is a snapshot of my application’s structure:

├── dist
├── email
│   └── johnoneone
│       └── dist
│       └── src
├── package.json
└── src
    └── services
        └── email
            ├── email-service.ts
            ├── facades
            │   ├── email.ts
            │   ├── production-email.ts
            │   └── test-email.ts
            ├── factory.ts
            ├── send-grid.d.ts
            └── templates
                ├── invite.ts
                └── template.ts
    └── test
        └── services
            └── email
                ├── email-service.ts
                ├── facades
                │   ├── email.ts
                │   ├── production-email.ts
                │   └── test-email.ts
                ├── factory.ts
                └── templates
                    ├── invite.ts
                    └── template.ts

A couple of things to note:

Class Diagram

To further model my solution I created a UML class diagram. I used StarUML to create the following:

Email template screen shot

Some design notes:

Implementation

Let’s send off the invite email. This is as simple as:

import { EmailTemplateFactory } from './services/email/factory';

let template = EmailTemplateFactory.invite;
template.firstName = invite.firstName;
template.inviter = `${user.firstName} ${user.lastName}`;
template.email.addTo(
  invite.email,
  `${invite.firstName} ${invite.lastName}`.trim(),
);
template
  .send()
  .then(() => {
    next();
  })
  .catch(next);

Note that I am using this in a REST API route method for Express, which is why you see the next() method being invoked when my promises is resolved or rejected.

It took a little bit of tweaking, but I really like this implementation. It’s simple and easy to use. Just get a template from the EmailTemplateFactory, set the substitution values, the recipient and then send it.

Email Class

The Email class represents an email that is being sent using SendGrid’s mail helper, which in turn uses their REST API. The Email is a facade for the SendGrid mail helper.

//import config
import { ConfigurationFactory } from "../../../config/factory";
import { IConfiguration } from "../../../config/config";

//import sendgrid
import * as SendGrid from "sendgrid";
import {
  SendGridContent,
  SendGridEmail,
  SendGridMail,
  SendGridPersonalization,
  SendGridResponse,
  SendGridSubstitution
} from "../send-grid";

/**
 * The base class for emails.
 * @class Email
 */
export abstract class Email {

  //constants
  public static FROM_EMAIL: string = "no-reply@johnoneone.com";
  public static FROM_NAME: string = "John 1:1";
  public static TO_EMAIL: string = "blove@johnoneone.com";
  public static TO_NAME: string = "Brian Love";

  //the config
  protected configuration: IConfiguration;

  //the SendGrid API
  protected sendGrid: any;

  //the SendGrid Mail helper
  protected _mail: any;

  /**
   * @constructor
   */
  constructor() {
    //get configuration object
    this.configuration = ConfigurationFactory.config();

    //store the SendGrid API
    this.sendGrid = SendGrid(this.configuration.sendGrid.key);

    //set default from email address(es)
    this.setFromString(Email.FROM_EMAIL, Email.FROM_NAME);
  }

  /**
   * Returns the Contents array.
   * @method get contents
   * @return {SendGridContent[]}
   */
  public get contents(): SendGridContent[] {
    return this.mail.getContents();
  }

  /**
   * Returns the from Email object.
   * @return {SendGridEmail}
   */
  public get from(): SendGridEmail {
    return this.mail.getFrom();
  }

  /**
   * Set the from email and name.
   * @method set from
   * @param {SendGridEmail} from
   */
  public set from(from: SendGridEmail) {
    this.mail.setFrom(from);
  }

  /**
   * Returns the populated SendGrid.mail.Email helper object.
   * @method get mail
   * @return {SendGridMail}
   */
  public get mail(): SendGridMail {
    //return existing mail object
    if (this._mail !== undefined) {
      return this._mail;
    }

    //set mail helper
    this._mail = new SendGrid.mail.Mail();

    return this._mail;
  }

  /**
   * Returns the SendGrid Personalization object.
   * @method get personalization
   * @return {SendGridPersonalization}
   */
  public get personalization(): SendGridPersonalization {
    //verify personalization exists
    if (this.mail.getPersonalizations() === undefined || this.mail.getPersonalizations().length === 0) {
      this.mail.addPersonalization(new SendGrid.mail.Personalization());
    }

    //get first personalization by default
    let personalizations = this.mail.getPersonalizations();
    return personalizations[0];
  }

  /**
   * Returns the subject for this email.
   * @method get subject
   * @return {string}
   */
  public get subject(): string {
    return this.mail.getSubject();
  }

  /**
   * Set the subject for this email.
   * @method set subject
   * @param {string} subject
   */
  public set subject(subject: string) {
    this.mail.setSubject(subject);
  }

  /**
   * Return the substitutions.
   * @method get substitution
   * @return {SendGridSubstitution[]}
   */
  public get substitutions(): SendGridSubstitution[] {
    return this.personalization.getSubstitutions();
  }

  /**
   * Add content to this email.
   * @method addContent
   * @param {SendGridContent} content
   * @return {Email}
   */
  public addContent(content: SendGridContent): Email {
    //add content to Mail helper
    this.mail.addContent(content);

    return this;
  }

  /**
   * Add content to this email from a simple string. The default type is "text/html".
   * @method addContentString
   * @param {string} value
   * @param {string} type
   * @return {Email}
   */
  public addContentString(value: string, type: string = "text/html"): Email {
    //build content
    let content: SendGridContent = {
      type: type,
      value: value
    };

    //add content to Mail helper
    this.addContent(content);

    return this;
  }

  /**
   * Add to address using simple values.
   * @method addTo
   * @param {string} email
   * @param {string} name
   * @return {Email}
   */
  public addTo(email: string, name?: string): Email {
    //create Email
    let to = new SendGrid.mail.Email(email);
    if (name !== undefined) {
      to.name = name;
    }

    //add to Mail helper
    this.personalization.addTo(to);

    return this;
  }

  /**
   * Add a substitution in the email template.
   * @method addSubstitution
   * @param {string} key
   * @param {string} value
   * @return {Email}
   */
  public addSubstitution(key: string, value: string): Email {
    let substition = new SendGrid.mail.Substitution(key, value);
    this.personalization.addSubstitution(substition);

    return this;
  }

  /**
   * Post-send hook.
   * @method postSend
   * @abstract
   */
  abstract post();

  /**
   * Pre-send hook.
   * @method preSend
   * @abstract
   */
  abstract pre();

  /**
   * Send the email.
   * @method send
   * @abstract
   */
  abstract send(): Promise<SendGridResponse>;

  /**
   * Set from using simple values.
   * @method setFromString
   * @param {string} email
   * @param {string} name
   * @return {Email}
   */
  public setFromString(email: string, name?: string): Email {
    //create Email
    let from = new SendGrid.mail.Email(email);
    if (name !== undefined) {
      from.name = name;
    }

    //set from property
    this.from = from;

    return this;
  }
}

Some notes:

ProductionEmail and TestEmail

The ProductionEmail and TestEmail classes extend the abstract Email class. These classes are used as appropriate in the EmailService.

When we are testing we do not want to ever send an email to the intended recipient. Rather, we send the mail to the TO_EMAIL address.

EmailTemplate Class

The EmailTemplate class represents an email template that is generated using ZURB’s Foundation for Emails (formerly Ink) project.

//import email
import { Email } from "../facades/email";
import { EmailService } from "../email-service";

//import file system i/o api
import { existsSync, readFileSync } from "fs";

//import sendgrid
import { SendGridContent, SendGridResponse } from "../send-grid";

/**
 * An email template.
 * The templates are generated by ZURB Foundation for Emails.
 * @class EmailTemplate
 */
export abstract class EmailTemplate {

  //the email dist path
  public static DIST_PATH: string = "email/johnoneone/dist";

  //the email
  public email: Email;

  //the email service
  public emailService: EmailService;

  //the email subject
  abstract subject: string;

  //the mime type
  public type: string = "text/html";

  //the contents of the template
  private _contents: SendGridContent;

  //the template file name
  private _fileName: string;

  /**
   * @constructor
   */
  constructor() {
    //create a new instance of the EmailService
    this.emailService = new EmailService();

    //store a reference to the Email
    this.email = this.emailService.email;
  }

  /**
   * Returns the SendGridContent object for this template.
   * @method get content
   * @return {SendGridContent}
   */
  public get content(): SendGridContent {
    //return content if it already exists
    if (this._contents !== undefined) {
      return this._contents;
    }

    //invoke pre-content hook
    this.pre();

    //build template file path
    var path: string = `${EmailTemplate.DIST_PATH}/${this.fileName}`;

    //verify template file exists
    if (!existsSync(path)) {
      throw new Error(`[EmailTemplate.content] The file does not exist {path: ${path}}.`);
    }

    //read file
    let value = readFileSync(path).toString();

    //build content
    let content: SendGridContent = {
      type: this.type,
      value: value
    };

    //invoke post-content hook
    this.post();

    return content;
  }

  /**
   * Returns the file name in the DIST_PATH directory for this template.
   * @method get fileName
   * @return {string}
   */
  public get fileName(): string {
    return this._fileName;
  }

  /**
   * Set the file name in the DIST_PATH directory for this template.
   * The contents of this file will be used for the email content.
   * @method set fileName
   * @param {string} fileName
   */
  public set fileName(fileName: string) {
    if (!fileName.match(/\.html$/i)) {
      fileName += ".html";
    }
    this.fileName = fileName;
  }

  /**
   * Post-content hook.
   * @method post
   * @abstract
   */
  abstract post();

  /**
   * Pre-content hook.
   * @method pre
   * @abstract
   */
  abstract pre();

  /**
   * Send this email template using the EmailService.
   * @method send
   * @abstract
   */
  public send(): Promise<SendGridResponse> {
    return this.emailService.populateFromTemplate(this).send();
  }

}

Let me explain the abstract EmailTemplate class:

SendInviteEmailTemplate Class

The SendInviteEmailTemplate class is a sample implementation of the abstract EmailTemplate class:

//import the base email class
import { EmailTemplate } from "./template";

/**
 * Invite email template.
 * @class SendInviteEmailTemplate
 */
export class SendInviteEmailTemplate extends EmailTemplate {

  //the first name of the person being invited
  public firstName: string = "";

  //the full name of the inviter
  public inviter: string = "";

  /**
   * Returns the email subject.
   * @method get subject
   * @return {string}
   */
  public get subject(): string {
    return `${this.inviter} has invited you to John 1:1 - a social app for Christians in small groups`;
  }

  /**
   * Returns the file name in the DIST_PATH directory for this template.
   * @method get fileName
   * @return {string}
   */
  public get fileName(): string {
    return "invite.html";
  }

  /**
   * Post-content hook.
   * @method post
   */
  public post() {
    //do nothing
  }

  /**
   * Pre-content hook.
   * @method pre
   */
  public pre() {
    //add custom substitutions
    this.email.addSubstitution("-firstName-", this.firstName);
    this.email.addSubstitution("-inviter-", this.inviter);
  }
}

This one is a bit easier to understand (hopefully):

EmailService Class

The EmailService class enables a developer to send off an email without using a template, as well as populating an email based on a template.

//import config
import { ConfigurationFactory } from "../../config/factory";
import { IConfiguration } from "../../config/config";

//import email
import { Email } from "./facades/email";
import { TestEmail } from "./facades/test-email";
import { ProductionEmail } from "./facades/production-email";

//import template
import { EmailTemplate } from "./templates/template";

//import sendgrid
import {
  SendGridContent,
  SendGridMail,
  SendGridResponse
} from "./send-grid";

/**
 * The exposed email service.
 * @class EmailService
 */
export class EmailService {

  //the email
  public email: Email;

  /**
   * @constructor
   */
  constructor() {
    //get configuration
    let configuration = ConfigurationFactory.config();

    //set email facade based on configuration
    if (configuration.isProduction()) {
      this.email = new ProductionEmail();
    } else {
      this.email = new TestEmail();
    }
  }

  /**
   * Returns the subject for this email.
   * @method get subject
   * @return {string}
   */
  public get subject(): string {
    return this.email.subject;
  }

  /**
   * Set the subject for this email.
   * @method set subject
   * @param {string} subject
   */
  public set subject(subject: string) {
    this.email.subject = subject;
  }

  /**
   * Add content to this email.
   * @method addContent
   * @param {SendGridContent} content
   * @return {Email}
   */
  public addContent(content: SendGridContent): EmailService {
    this.email.addContent(content);
    return this;
  }

  /**
   * Add content to this email from a simple string. The default type is "text/html".
   * @param {string} value
   * @param {string} type
   * @return {Email}
   */
  public addContentString(value?: string, type: string = "text/html"): EmailService {
    this.email.addContentString(value, type);
    return this;
  }

  /**
   * Add to address using simple values.
   * @method addTo
   * @param {string} email
   * @param {string} name
   * @return {Email}
   */
  public addTo(email: string, name?: string): EmailService {
    this.email.addTo(email, name);
    return this;
  }

  /**
   * Add a substitution in the email template.
   * @method addSubstitution
   * @param {string} key
   * @param {string} value
   * @return {Email}
   */
  public addSubstitution(key: string, value: string): EmailService {
    this.email.addSubstitution(key, value);
    return this;
  }

  /**
   * Populate the email using a template.
   * @method populateFromTemplate
   */
  public populateFromTemplate(template: EmailTemplate): EmailService {
    //add content to email from template
    this.email.addContent(template.content);

    //set the subject from the template
    this.email.subject = template.subject;

    return this;
  }

  /**
   * Send the email.
   * @method send
   * @abstract
   */
  public send(): Promise<SendGridResponse> {
    return this.email.send();
  }

  /**
   * Set from using simple values.
   * @method setFromString
   * @param {string} email
   * @param {string} name
   * @return {EmailService}
   */
  public setFromString(email: string, name?: string): EmailService {
    this.email.setFromString(email, name);
    return this;
  }

}

Some notes:

Tests

I wrote tests using mocha, chai and mocha-typescript. You can run the test via:

$ npm test

Conclusion

When building this I had to duplicate a lot of the SendGrid interfaces from the node_modules/sendgrid/index.d.ts definition file as most of them are not publicly accessible. I created a send-grid.d.ts file that includes all of the definitions that I need.

As always, please feel free to contribute or to comment your suggestions!

Download

You can download the source code or fork the repository on GitHub: