Brian Love
Angular + TypeScript Developer in Denver, CO

Node.JS + SendGrid + TypeScript

Reading time ~16 minutes

(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:

  • -firstName-
  • -inviter-
  • -groupName-

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:

  • The root dist directory is the output directory for our TypeScript that is in the src directory. This will be executed by Node. We’ll be focusing on the source code, though.
  • The root email directory contains the Foundation for Emails project johnoneone. We will reference the dist directory when adding content to our emails using a template.
  • The root src directory contains our TypeScript application source. I am using the Express framework for Node.js.
  • Within the src directory is a services directory, and within that I have a directory for our email service.
  • The email-service.ts file is the module for our EmailService class. We’ll review that shortly.
  • The factory.ts is a simple factory that will create email templates, such as the SendInviteEmailTemplate class.
  • The facades directory contains a base Email class (in the email.ts module) as well as the ProductionEmail and TestEmail classes in production-email.ts and test-email.ts.
  • The templates directory contains a base Template abstract class as well all of the templates that we will define. Right now I have a single template defined called SendInviteEmailTemplate.

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:

  • The abstract Email class represents an email and is a facade for the SendGrid mail helper. The ProductionEmail class will send the email as is. The TestEmail class is used when testing, and will delete the Mail object and create a new one that is sent to the TO_EMAIL address. This is to prevent sending emails to clients when testing.
  • The EmailTemplate class represents a template. This is to use the ZURB Foundation for Emails templates that are built. The plan is that each template is built using Foundation for Emails and then an associated class is created that extends EmailTemplate. The implementation will provide the file name for the html template as well as substituting any values in the template.
  • The EmailService exposes the Email facade in a single API to the rest of my application. The EmailService can send simple/text emails as well as work with an EmailTemplate to populate the content and subject.
  • The EmailTemplateFactory is a singleton that has static methods for getting a template.

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:

  • I use a ConfigurationFactory to store the API key for my application. You can disregard this if you prefer, and just hard code the value from SendGrid in your code.
  • The FROM_EMAIL and FROM_NAME static strings are the default values when sending an email. You can always override these. You will want to change this to match your needs.
  • The TO_EMAIL and TO_NAME are the default when sending emails in test mode.
  • The contents accessor returns the array of Contents in the email.
  • The from mutator and accessor expose the from Email object.
  • The mail accessor returns the singleton SendGrid.mail.Mail instance.
  • The personalization accessor returns the Personalization object. This is a helper method for adding substitutions.
  • The subject mutator and accessor expose the subject string.
  • The substitutions accesor exposes the substitutions object. Note here that I have defined this as an array of Substitution objects. This does not appear to be the case and I have submitted a PR for the TypeScript definition file.
  • The addContent() method enables the developer to directly add Content to the email.
  • A helper method named addContentString allows you to add HTML string content to the email easily.
  • The addTo() method adds a new recipient to the email.
  • The addSubstitution() method adds a key/value pair for substituting values in your email template.
  • The post() and pre() methods are abstract and must be implemented in an extending class.
  • The setFromString() is a helper method to set the from Email object.

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:

  • First I import the necessary functions and classes. Note that I am importing the existsSync and readFileSync functions from the file system Node package.
  • The static variable DIST_PATH is the relative path to the dist directory for my Foundation for Emails build directory.
  • The email and emailService properties store reference to the Email object as well as our EmailService.
  • The subject string property is abstract and must be defined in an extending class.
  • The contents property is also abstract and must be implemented in an extending class.
  • The fileName string property is also abstract. The extending class will instruct the EmailTemplate on which template file we are using.
  • The post() and pre() hooks are abstract and will be invoked before and after the content is generated from the template. The pre() method gives us an opportunity to wire up any substitutions before we generate the content. The post() method provides us with an opportunity to clean anything up.
  • The send() method uses the EmailService to populate an Email using the template and to send it.

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):

  • The firstName and inviter string properties contain the values that will be substituted into the email template. The developer using this class will set the value of these properties before sending the email.
  • First, I override the accessor for the subject property to return a custom subject for the email.
  • Second, I override the accessor for the fileName property to return the template file name.
  • Finally, I implement the pre() and post() hooks. The pre() method wires up the substitutions.

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:

  • Again, I am using a configuration object for determining if my application is running in production or test mode. This is based on an environment variable.
  • The email property is either a ProductionEmail or a TestEmail object. This is created in my constructor() function.
  • Several of the properties and methods that are in the Email class are exposed in the EmailService, including subject, addTo(), addSubstitution() and setFromString().
  • The populateFromTemplate() method accepts an EmailTemplate object and populate the Email content and subject from it.
  • The send() method invokes the mail helper’s send() method, returning the Promise object.

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:

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).