Add transactional emails to a JavaScript web app (React, Express) with AWS SES (Technical)

Author: | Created on: Mar 5th 2019 | Last edited on: 3 days ago


Async

Sending transactional emails is a basic feature of modern web applications. Apps send emails for various reasons. A simple app may send only a welcome email to a newly registered user, while a sophisticated SaaS app may send multiple email alerts to business customers. Since sending transactional emails is not what differentiates software, many developers opt to outsource it. One great option is Simple Email Service by AWS launch (AWS SES). In our experience, AWS SES has proved to be a robust technology, plus you get 62,000 emails per month for free.

In this tutorial, I will walk you through AWS SES integration with a simple React-Express app. This tutorial assumes basic knowledge of React, JavaScript, Express, and HTTP.

In a real-world situation, an app would send transactional emails to existing app users. Thus, the app would retrieve an email address from a user document in a database. In this tutorial, however, our simple app does not have user authentication, a User Model, nor a connection to a database. To get an email address, we will create a single-input form that accepts email addresses. When a user submits this form, our app will send a welcome email via AWS SES to the email address provided in the form.

At the beginning of this tutorial (see below instructions), I’ll provide you with code for a simple JavaScript app that is build with React, Material-UI, Next, and Express. The app has a SendEmail page with the route /send-email. This page has a form that, on a submit event, accepts an email address and passes it to a sendEmailToServer API method. We built this page using React, Material-UI, and Next in our previous tutorial, where we discussed Mailchimp API.

Async

So you have a page, but here is a list of things you will build to integrate this app with AWS SES:

  • define an API method sendEmailToServer using thefetch` method
  • define an Express route /api/v1/public/send-email
  • define a sendEmail API method that calls theses.sendEmailmethod fromaws-sdk`
  • test out integration with the free tool Postman launch and manually

You start this tutorial with a simple app. If you don’t have time to run the app locally, I deployed this example app at: https://aws-ses.builderbook.org/send-email launch

To run the app locally, get the app code from the tutorials/2-start launch folder of our builderbook repo launch .

  • Clone the builderbook repo to your local machine with:

    git clone git@github.com:builderbook/builderbook.git
  • Inside the tutorials/2-start folder, run yarn or npm install to install all packages listed in package.json.

To integrate AWS SES with our app, you will install and learn about the following third party packages:


SendEmail page

We discussed in detail and built the Subscribe page from scratch in our tutorial on Mailchimp API. Our SendEmail page has exactly the same structure. For this tutorial, we simply:

  • renamed the page component from Subscribe to SendEmail
  • changed the route from /subscribe to /send-email.

Find the SendEmail page's code at pages /send-email.js. In Next.js, the page’s route is the same as the name of the page's file.

After installing packages, start your app with yarn dev and access this page: http://localhost:8000/subscribe launch .

Though not in the scope of this tutorial, go ahead and try changing the values passed to different props of the TextField and Button components from Material-UI. For example, change text for the label prop to Type your email and change the Button variant prop to flat:

Async

Having Material Design out of the box is great for our productivity.


sendRequestToServer API method

Take a look at the page's code (pages/send-email.js), specifically this part with try/catch and async/await:

try {
  await sendRequestToServer({ email });

  // some code, not relevant
}

After a user submit the form, our code passes the user’s email address to a sendRequestToServer API method. The code then calls and waits for that method. In this section of the tutorial, our goal is to define this sendRequestToServer API method.

From the import section of pages/send-email.js. You see that:

import { sendRequestToServer } from '../lib/api/public';

We will define sendRequestToServer at lib/api/public.js. To make sendRequestToServer universally available, meaning available on both server and client (browser), we place it into our lib folder. We do so because on initial load, some pages in Next.js are server-side rendered. Thus, API methods should be available on both server and client.

In the code of our previous tutorial launch and in Chapter 2 of our book launch , we discussed in detail and constructed a sendRequest method:

async function sendRequest(path, options = {}) {
  const headers = {
    'Content-type': 'application/json; charset=UTF-8',
  };

  const response = await fetch(
    `${ROOT_URL}${path}`,
    Object.assign({ method: 'POST', credentials: 'include' }, { headers }, options),
  );

  const data = await response.json();

  if (data.error) {
    throw new Error(data.error);
  }

  return data;
}

Nevertheless, let's go over this method:

  • To send a request and a receive response, sendRequest calls and waits for the fetch method. JavaScript's fetch launch is a global method that fetches data over a network by sending a request and receiving a response.
  • We use the isomorphic-fetch package that makes fetch universally available in our app. Install this package with: yarn add isomorphic-fetch
  • The sendRequest method adds the Content-type header launch with the value application/json; charset=UTF-8 to a request. With this header, the client tells the server that data inside the request is in JSON format.
  • The method sends a request to ${ROOT_URL}${path}. We haven't define these values yet.
  • For local development, ROOT_URL is http://localhost:8000. path is the so-called API endpoint. It can be any unique route, and we chose it to be /api/v1/public/send-email.
  • By default, the request's method launch is POST and credentials launch is include.
  • The entire request object is created out of 3 smaller objects using the Object.assign launch method. These 3 objects are: { method: 'POST', credentials: 'include' }, { headers }, options..
  • The object options is empty by default, options = {}. In our case it will be: {body: JSON.stringify({email})}

Create a lib/api/public.js file and define sendRequestToServer as follows: lib/api/public.js :

import 'isomorphic-fetch';

const ROOT_URL = 'http://localhost:8000';

async function sendRequest(path, options = {}) {
  const headers = {
    'Content-type': 'application/json; charset=UTF-8',
  };

  const response = await fetch(
    `${ROOT_URL}${path}`,
    Object.assign({ method: 'POST', credentials: 'include' }, { headers }, options),
  );

  const data = await response.json();

  if (data.error) {
    throw new Error(data.error);
  }

  return data;
}

export const sendRequestToServer = ({ email }) =>
  sendRequest('/api/v1/public/send-email', {
    body: JSON.stringify({ email }),
  });

We've defined our sendRequestToServer API method using sendRequest (which is reusable, modifiable, and can be used by any universally-available API method in our app). We did not specify method, so the request's method is POST by default. We specified the API endpoint and added an email address in JSON format to the request's body.


Express route /send-email

At this point, we wrote all code we need to send a request to the server and receive a response from the server. Our next step is to write code on our Express server that handles the request and sends a response back to an API method on the client (this is true for client-rendered pages, and our SendEmail page is indeed client-side rendered). To handle a request at a particular API endpoint, we need to create an Express route.

Syntax for a basic Express route launch :

server.METHOD('path', ...);

You already have enough information to put together a basic Express route:

  • The METHOD is POST.
  • The path is an API endpoint, which we chose to be /api/v1/public/send-email.
  • (req, res) is an anonymous arrow function.
  • See how we used try/catch and async/await constructs inside the SendEmail page (pages/send-email.js). For a deeper dive into async/await, Promise.then, and asynchronous callbacks - check Chapter 3 of our book launch and stay tuned for our future tutorials.

Plug in the above values and you get:

server.post('/api/v1/public/subscribe', async (req, res) => {
  try {
    // await sendEmail();
    res.json({ sent: 1 });
    console.log('non-error response is sent'); // eslint-disable-line no-console
  } catch (err) {
    res.json({ error: err.message || err.toString() });
  }
});

A couple of notes:

  • We wrote error: err.message || err.toString() to handle two error situations - when the error is a type of string, and when the error is an object.

  • To test out our Express route, we added this line:

    console.log('non-error response is sent');
  • sendEmail is a server-side API method that will call the AWS SES API method ses.sendEmail, which in return will send a server-to-server POST request that contains an email address. This request is from our server to AWS SES’s server.

Place your Express route right above this line in our main server code at server/app.js:

server.get('*', (req, res) => handle(req, res));

We are ready to test.

We won't test the entire flow (from submitting a form to getting an actual email) just yet. Before that, we should make sure that our API endpoint works as expected. I recommend using the Postman app launch for testing out a request-response cycle of our new API endpoint.

Open Postman, create POST request, take at other request properties:

Async

You need to specify at least 3 request properties on Postman. These properties are similar to those in the sendRequest method inside lib/api/public.js:

  • method is POST
  • specify the full path for the API endpoint: http://localhost:8000/api/v1/public/subscribe
  • add a Content-Type header with the value application/json

Start your app with yarn dev. Go back to Postman and click Send.

If successful, you will see the following two outputs:

On Postman, you see the response has code 200 (on the right), and the following body:

Async
  1. Terminal prints:
Async

Good job. You just wrote a working Express route and working internal API endpoint!

You just demonstrated that two events happen successfully in our app:

  • a request gets sent
  • a response is received

However, we did not pass an email address to a function inside our Express route. We need an actual email address to send an email. The request that our sendRequestToServer method sends contains email address as req.body.email. This is how we designed our sendRequestToServer API method. Let's define a local variable email and give it a value of email address:

const email = req.body.email;

Use ES6 object destructuring for shorter syntax:

const { email } = req.body;

Throw an error and exit with blank return, if, for some reason, the `email local variable does not exist:

if (!email) {
  res.json({ error: 'Email is required' });
  return;
}

Also, modify the console.log statement to print out email.

After these modifications of your Express route, you get:

server.post('/api/v1/public/send-email', async (req, res) => {
    const { email } = req.body;
    if (!email) {
      res.json({ error: 'Email is required' });
      return;
    }
    try {
      // await sendEmail();
      res.json({ sent: 1 });
      console.log(email); // eslint-disable-line no-console
    } catch (err) {
      console.log('Email sending error:', err); // eslint-disable-line no-console
    }
  });

Let’s test it out. Open Postman, and add one more property to our request: body with the value team@builderbook.org. Make sure that you selected raw > JSON data format:

Async

Make sure that your app is running and then click the Send button.

Look at the response on Postman and the output of your terminal:

Postman will display Loading... but never finish.
Terminal outputs an error: TypeError: Cannot read property 'email' of undefined
This means that the local variable email is undefined. To read the email property from req.body, you need to add a parser that decodes the body object of a request from Unicode to JSON format. This parser is called bodyParser launch .

Add the bodyParser package:

yarn add body-parser

Import it to server/app.js with:

import bodyParser from 'body-parser';

Mount JSON bodyParser on the server. Add the following line before your Express route:

server.use(bodyParser.json());

An alternative to using the external bodyParser package is to use internal Express middleware. To do so, remove the import code for bodyParser and replace the above line of code with:

server.use(express.json());

Let’s test it. Make sure your app is running and click the Send button on Postman.

Look at the response on Postman and your terminal:

Postman succesfully outputs: "sent": 1
Terminal prints team@builderbook.org launch without any error.
Great, now our server parses and decodes the request’s body.

In the final section of this tutorial, we’ll define a server-side sendEmail method that we call and wait for in the above Express route. This sendEmail method calls the AWS SES ses.sendEmail API method. The latter method sends an authorized POST request from our server to AWS SES’s server.


Server-side API method sendEmail

We send a server-to-server POST request inside a server-side API method. For example, in our book, we do so to integrate our app with GitHub and Mailchimp. However, for some integrations, such as Google OAuth or Stripe, we do not write any code that sends a POST request to third party servers. Instead we use popular and trusted packages that send requests internally. This is exactly the case with AWS SES, which uses the aws-sdk package.

Add this package with:

yarn add aws-sdk

Next:

  • Create a server/aws.js file.
  • Import aws-sdk.
    server/aws.js :
    import aws from 'aws-sdk';
    // more code after we discuss it

We will use the ses.sendEmail method from AWS SES API to send a welcome email. Before we jump to defining ses.sendEmail, we should configure our aws service. Take a look at the proper syntax in the official AWS SES docs launch for AWS SES API. To configure, we need to supply region, accessKeyId, and secretAccessKey.

server/aws.js :

aws.config.update({
  region: 'us-east-1',
  accessKeyId: process.env.Amazon_accessKeyId,
  secretAccessKey: process.env.Amazon_secretAccessKey,
});

process.env.Amazon_accessKeyId means that Amazon_accessKeyId is an environmental variable. At the end of this section, we will discuss where to store values for sensitive environmental variables.

After configuring our aws service, SES API is accessed as a function on the service:

const ses = new aws.SES({ apiVersion: 'latest' });

The method we want to use is ses.sendEmail. This method takes multiple parameters, for example:

  • Source
  • ToAddresses
  • CcAddresses
  • BccAddresses
  • ReplyToAddresses
  • Subject
  • Body and more launch .

Let’s define our sendEmail API method with 6 parameters for ses.sendEmail. However, when we actually pass values to sendEmail, we can choose to pass values to a subset of these parameters.

6 parameters for ses.sendEmail:

  • Source (options.from)
  • ToAddresses (options.to, nested in the Destination)
  • CcAddresses (options.cc, nested in the Destination ),
  • Data (options.subject, nested in the Message>Subject)
  • other Data (options.body, nested in the Message>Body>Html)
  • ReplyToAddresses (options.replyTo)

Here is our ses.sendEmail with 6 parameters:

ses.sendEmail(
  {
    Source: options.from,
    Destination: {
      CcAddresses: options.cc,
      ToAddresses: options.to,
    },
    Message: {
      Subject: {
        Data: options.subject,
      },
      Body: {
        Html: {
          Data: options.body,
        },
      },
    },
    ReplyToAddresses: options.replyTo,
  },
)

Time to put all the above code snippets together: configuration of service, initialization of a new service with the latest version of AWS SES API, and the ses.sendEmail API method with 6 parameters.

Let’s make the sendEmail function return a Promise. That way we can wait for it inside our Express route by using async/await. If err is returned after the server calls ses.sendEmail - the Promise returns reject(err). If info is returned without err - the Promise returns resolve(info).

server/aws.js :

import aws from 'aws-sdk';
const Amazon_accessKeyId = 'xxxxxx';
const Amazon_secretAccessKey= 'xxxxxx';
export default function sendEmail(options) {
  aws.config.update({
    region: 'us-east-1',
    accessKeyId: Amazon_accessKeyId,
    secretAccessKey: Amazon_secretAccessKey,
  });
  const ses = new aws.SES({ apiVersion: 'latest' });
  return new Promise((resolve, reject) => {
    ses.sendEmail(
      {
        Source: options.from,
        Destination: {
          CcAddresses: options.cc,
          ToAddresses: options.to,
        },
        Message: {
          Subject: {
            Data: options.subject,
          },
          Body: {
            Html: {
              Data: options.body,
            },
          },
        },
        ReplyToAddresses: options.replyTo,
      },
      (err, info) => {
        if (err) {
          reject(err);
        } else {
          resolve(info);
        }
      },
    );
  });
}

You may ask why the region is us-east-1. Many services, including AWS SES, are provided on a per-region basis. If you plan to send emails from servers located in a particular region, select that region on your AWS dashboard and specify the selected region in the above code.

At this point, the only undefined variables are Amazon_accessKeyId and Amazon_secretAccessKey. To get values for these keys, sign up for AWS launch and navigate to AWS SES.

You want to achieve the following:

  • verify your email address (SES will email users on behalf of this email address)
  • generate an AWS access key and secret access key

Below, I walk you through each step:

Go to Email Addresses in your SES dashboard. Check the snapshot below to find the link on your dashboard:

Async

Click the dark blue button Verify a New Email Address. Follow the instructions. Once your email address is verified, you can use it to send emails. You will be able to send test emails from the SES dashboard, as well as from your app using SES API.

Important note: it may take some time for AWS SES to verify your email. Our app will use this verified email as the from email address for transactional emails.

  1. To access AWS API from our app, we need to generate an access key and secret access key at Security Credentials launch . Follow the snapshot below to access your Security Credentials:
    Async

Open the section Access keys (access key ID and secret access key) and click the dark blue button Create New Access Key. Copy your access key and secret access key. Keep them in a safe place! After you create your keys, you won't be able to find or edit your secret access key. If you lose your secret access key, you'll have to generate a new pair of keys.

Once you have the keys, add their values to server/aws.js.

Good job if you got this far — we are done with our server-side sendEmail API method!


Testing

Before we test, let’s import and use our sendEmail method in the Express route that we wrote earlier.

Import to server/app.js with:

import sendEmail from './aws';

Add sendEmail to your Express route:

server.post('/api/v1/public/send-email', async (req, res) => {
  const { email } = req.body;
  if (!email) {
    res.json({ error: 'Email is required' });
    return;
  }
  const template = {
    // here we define email template
  };
  try {
    await sendEmail({
      from: 'Mr.Hanky from Builder Book <team@builderbook.org>',
      to: [email],
      subject: template.subject,
      body: template.message,
    });
    res.json({ sent: 1 });
    console.log(email);
  } catch (err) {
    res.json({ error: err.message || err.toString() });
  }
});

A note on [email] - we pass the email as an array with a single string to satisfy an AWS SES API requirement.

Let’s define the local variable template:

server.post('/api/v1/public/send-email', async (req, res) => {
  const { email } = req.body;
  if (!email) {
    res.json({ error: 'Email is required' });
    return;
  }
  const template = {
    subject: 'Welcome to builderbook.org',
    message: `Hooowdy Ho,
      <p>
        Thank you for signing up on our website!
      </p>
      <p>
        Have a good day.
      </p>
      Mr. Hanky
    `,
  };
  try {
    await sendEmail({
      from: 'Mr.Hanky from Builder Book <team@builderbook.org>',
      to: [email],
      subject: template.subject,
      body: template.message,
    });
    res.json({ sent: 1 });
    console.log(email); // eslint-disable-line no-console
  } catch (err) {
    console.log('Email sending error:', err); // eslint-disable-line no-console
  }
});

Save your changes server/app.js. Now we are ready to test!

At this point, you may skip testing with Postman.

If you aren’t running the app locally, you can go to the example app I deployed for this tutorial: https://aws-ses.builderbook.org/send-email launch

If you are running the app locally:

I got my email:

Async

You just learned a few handy skills: building an internal API and sending transactional emails with AWS SES.

When you complete this tutorial, your code should match code in the tutorials/2-end folder launch . This folder is located in the tutorials directory of our builderbook repo launch .

If you found this article useful, consider giving a star to our Github repo launch and checking out our book launch where we cover this and many other topics in detail.

If you are building a software product, check up our SaaS boilerplate launch and Async launch .