Get an Email with Today’s Extreme Stock Movers Using Node.js, IEX, and Twilio SendGrid

September 23, 2020
Written by
Bonnie Schulkin
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

stockmovers.png

Everyone knows the Twilio API for SMS messaging. But did you know that you can also send email via Twilio? The Twilio SendGrid email API has all the ease of use you expect from Twilio, combined with the power of the SendGrid email engine.

In this post, you’ll write a Node.js script to:

To get the email daily, you can run the script with your favorite scheduler, i.e. cron, Task Scheduler, or launchd.

Prerequisites

  • You'll need Node.js installed on your computer. The SendGrid wrapper supports back to version 6, but they recommend using the latest version (12).

Start the Node.js Project

Note: you can find the finished project in this GitHub repo.

To start, you’ll need to do some not-very-interesting "creating a Node.js project" stuff. First, make a project directory called twilio-stock-email. Then cd into it, and initialize git and npm:

mkdir twilio-stock-email
cd twilio-stock-email
git init
npm init

You can either fill out the metadata for the npm init, or hit enter to accept each default.

Install Dependencies

Luckily, both the Twilio SendGrid Email API and the IEX Cloud API have wrappers for Node. Wrappers are packages that provide functions for using an API, rather than needing to make the HTTP calls yourself. You can find the wrapper documentation for these packages at Twilio SendGrid Email API Quickstart for Node.js and IEX Node.js wrapper GitHub.

To make the code available to your project, install these packages:

npm install @sendgrid/mail
npm install iexcloud_api_wrapper

.env File

In a minute, you’ll be getting access keys from IEX and SendGrid. You’ll want to keep your SendGrid API key off of GitHub, so that spammers can't use it for their own nefarious purposes (you’ll want to keep your IEX info safe from bad actors too). There are different ways to do this, but in this tutorial, you'll store API keys in a .env file, use the dotenv npm package to access the secret values, and make sure the .env file doesn't get uploaded to GitHub.

First, create a .env file in your project.

Next, install the dotenv package by running npm install dotenv from your command prompt.

Keep the .env file out of GitHub by creating a .gitignore file at the top level of your project. Update the .gitignore contents with a single line that says .env (this will tell GitHub to ignore files whose names match this pattern).

Actually, while you're in there, add a second line that says node_modules. There’s no need to store those in GitHub, since they're easily recreated from package.json.

First Git Commit

It’s time to make sure you're not going to commit those lovely API keys (or node modules) to git.

Run the following command:

git status

Your output should look something like this:

‘git status’ output on the command line, with no mention of .env or node_modules

No .env? No node_modules? Great, it's safe to commit:

git add .
git commit -m "Set up project"

Getting the Keys

Now it’s time to get the API keys to populate your .env file (of course, if you already have authentication for IEX or SendGrid, you can use the keys you already have).

SendGrid

First, create an account at https://signup.sendgrid.com. When the signup is complete, you’ll see an account dashboard.

After basking in your stellar reputation, go to Settings → API Keys on the sidebar:

 Screenshot of Settings → API Keys on the sidebar

Click the "Create API Key" button. When prompted, give your API key a name and then select “Full Access” for permissions.

Screenshot of “Create API Key button in the upper right.

Copy the API key into your .env file like so:

SENDGRID_API_KEY = your_sendgrid_api_key

You may also want to save this API key in a backup location (they aren't kidding when they say they won't show it to you again). I use LastPass to manage passwords, so I pasted the key into the "Notes" section for my SendGrid password record in LastPass.

IEX Cloud

To start, sign up for an Individual account.

After you create your account, you'll be asked to select a plan. You’re just testing the waters here, and don't need to plunk down any money just yet, so choose "Select Start" under "Get started for free":

Screenshot of “Select Start under “Get started for free on IEX plan page

Check your email and click the verification link they sent (you can't get your API keys without a verified email).

Screenshot of directions to click link on verification email, from IEX site

Close any pop-ups (you may get one introducing the IEX Cloud Console) after email verification.

Copy your public key to your .env file.

Screenshot of account number and API token on the IEX Console page
IEXCLOUD_PUBLIC_KEY = your_iexcloud_public_key

Click "Go to API Tokens", then click "Reveal secret token" for your private keys:

Screenshot of “Reveal secret token button on the IEX API Tokens page

Add this key to your .env file, too, plus the version (we’ll go with “stable”).

IEXCLOUD_SECRET_KEY = your_iexcloud_secret_key
IEXCLOUD_API_VERSION = "stable"

Getting Data from IEX Cloud

Ok, now it's time for the fun stuff. First, make a file for your script called daily-stock-email.js.

touch daily-stock-email.js

You’ll start by importing the IEX Cloud wrapper, and making a function to retrieve the stock data.

const iex = require('iexcloud_api_wrapper'); // gets auth from .env automatically

const getMoverData = async() => {

}

A couple notes here:

The iex_cloud_wrapper automatically looks in the .env file for your API keys, so you don't even have to include them explicitly in the script!

Since the methods for iex_cloud_wrapper are async, you’ll need to make the function async too, in order to use await.

Looking at the Data

Looking at the IEX Cloud API docs, I can see that the list endpoint will help us get the day's biggest gainers and losers (see the "examples" for this endpoint for gainers and losers).

Following the example shown in the IEX Node.js wrapper README file, add code to getMoverData() that retrieves the gainers and (temporarily) logs the response to the console:

const getMoverData = async() => {
  try {
    const gainers = await iex.list('gainers');
    console.log(gainers);
  }
  catch(error) {
    console.error(`Could not get data: ${error}`);
    process.exit(-1);  // nonzero exit code indicates failure
  }
};

// temporary call to getMoverData() to look at output
getMoverData();

Now run the script and see what happens!

node daily-stock-email.js

Your script should have generated several hundred lines of output containing an array of Quote objects, each object representing one of the top ten gainers for the day. Great, knowing this format will be useful once it’s time to start processing the data. Here's the beginning of the output I got:

screenshot showing stock data output

If you got a 404 error, check your .env file and make sure you don't have any mistakes in your key names or values.

Finishing the Function

To finish the getMoverData() function, let's get the stock losers too, and then return an object that contains both responses. Eventually, you’ll pass this object to a function that will generate the HTML.

const getMoverData = async() => {
  try {
    const gainers = await iex.list('gainers');
    const losers = await iex.list('losers');
    return { gainers, losers };
  }
  catch(error) {
    console.error(`Could not get data: ${error}`);
    process.exit(-1);  // nonzero exit code indicates failure
  }
}

Finally, you’ll remove the temporary getMoverData() call below the getMoverData() function declaration (that call was only there so you could see what the output looked like). In its place, you’ll write the main function that will be called when the script runs.

const main = async () => {
  const { gainers, losers } = await getMoverData();
}

main()
  .catch(error => console.error(error));

Note the snazzy destructuring of the getMoverData() object output. 😉

Formatting the Data into HTML Tables

So you could just mail those hundreds of lines to yourself, but your future self might not need to know all of that information. To help your future self out, you’ll parse out only the data you’re interested in. Even better, you'll display the parsed data in HTML tables for easy viewing.

You'll start with a generateTable() helper function, which you’ll use to make tables from the data returned by IEX Cloud.

For the generateTable() function, I chose some properties I was interested in. Feel free to customize the table to your own interests!

First, create a generateTable() function, immediately after the getMoverData() function. It will take one argument, stockData, (either the gainers or losers value that is returned from getMoverData()).

const generateTable = (stockData) => {
  
}

Then create the skeleton of the table, including hard-coded column headers:

const generateTable = (stockData) => {
  return `
    <table>
      <thead>
        <tr>
          <th>% Change</th>
          <th>Symbol</th>
          <th>Company</th>
          <th>Close</th>
          <th>Previous Close</th>
          <th>YTD Change</th>
        </tr>
      </thead>
      <tbody>
        // actual data will go here
      </tbody>
    </table>
  `

Finally, you’ll use the array method map() to create rows from the data:

const generateTable = (stockData) => {
  const rows = stockData.map(data => 
    `<tr>
      <td>${Math.round(data.changePercent * 10000) / 100}</td>
      <td>${data.symbol}</td>
      <td>${data.companyName}</td>
      <td>${data.latestPrice}</td>
      <td>${data.previousClose}</td>
      <td>${Math.round(data.ytdChange * 100) / 100}</td>
    </tr>`
  ).join('\n');
  return `
    <table>
      <thead>
        <tr>
          <th>% Change</th>
          <th>Symbol</th>
          <th>Company</th>
          <th>Close</th>
          <th>Previous Close</th>
          <th>YTD Change</th>
        </tr>
      </thead>
      <tbody>
        ${rows}
      </tbody>
    </table>
  `

The script uses Math.round() to take numbers with too many digits and pare them down.

To achieve a ytdChange value for the table that only had two digits after the decimal point, I used a rounding technique in JavaScript where the value is multiplied by 100, rounded to the nearest integer using Math.round() and then divided by 100.

For changePercent, I used a similar technique, but multiplied by 10000 because the value returned from IEX is a decimal between 0 and 1, and I wanted the value to be represented as a percentage between 1% and 100%.  

Incorporating the Tables into an HTML Email

All right, now that you have a way to make the tables, it’s time to make a function to generate the full HTML. You’ll generate the tables using the generateTable() helper function, and surround it with some explanatory headers.

Make a function called generateHtml() and put it right after getMoverData(), before generateTable(). This new function will take two arguments, gainers and losers, which will come from getMoverData(). You’ll need those to pass to generateTable() individually.

const generateHtml = (gainers, losers) => {
  const gainerTable = generateTable(gainers)
  const loserTable = generateTable(losers)
  return `<html>
    <body>
      <h1>Today's Biggest Stock Movers</h1>
      <h2>Gainers</h2>
      <div>${gainerTable}</div>
      <h2>Losers</h2>
      <div>${loserTable}</div>
    </body>
  </html>`
}

I'll add some CSS because I like my tables with lines, but you do you. 😄

const generateHtml = (gainers, losers) => {
  const gainerTable = generateTable(gainers)
  const loserTable = generateTable(losers)
  return `<html>
   <head>
      <style>
        table, th, td {
          border: 1px solid black;
          border-collapse: collapse;
          padding: 3px;
        }
      </style> 
    </head>
    <body>
      <h1>Today's Biggest Stock Movers</h1>
      <h2>Gainers</h2>
      <div>${gainerTable}</div>
      <h2>Losers</h2>
      <div>${loserTable}</div>
    </body>
  </html>`
}

Now, update main() so it calls generateHtml() and passes to it the destructured values returned from getMoverData():

const main = async () => {
  const { gainers, losers } = await getMoverData();
  const htmlEmailContents = generateHtml(gainers, losers);
}

Sending the Email

The moment has arrived! You have the HTML all ready, and it just needs a function that will send it via email.

Let's take a look at the Twilio SendGrid Email API Quickstart for Node.js to see what's what.

First, set up the SendGrid Node.js wrapper at the top of the file:

require('dotenv')
const iex = require( 'iexcloud_api_wrapper' ) // gets auth from .env automatically
const sgMail = require('@sendgrid/mail')
sgMail.setApiKey(process.env.SENDGRID_API_KEY)

require('dotenv') is what allows us to access the SendGrid API key from your .env file with process.env.SENDGRID_API_KEY.

Now that that's out of the way, you can use the example from the Quickstart Guide to make a function of your own. The function needs the async keyword because sgMail.send() is an async function and I want to use the await syntax.

const sendEmail = async (htmlEmailContents) => {
  const msg = {
        to: process.env.EMAIL,
        from: process.env.EMAIL,
        subject: 'Today\'s biggest stock market movers',
        html: htmlEmailContents,
  };
  try {
        await sgMail.send(msg);
  }
  catch (error) {
        console.error(`Could not send message: ${error}`);
  }
}

Note that you’re getting the "to" and "from" email from .env too. I don’t know about you, but I do not like spammers enough to give them my email in GitHub.

To avoid this, you’ll need to add a line to .env that looks like this:

EMAIL = "your_email_address"

Add a call to sendEmail to the end of main():

const main = async () => {
  const { gainers, losers } = await getMoverData();
  const htmlEmailContents = generateHtml(gainers, losers);
  await sendEmail(htmlEmailContents);
}

Finally, print out a success message on successful call of main():

main()
  .then(() => console.log(`Sent stock mover email to ${process.env.EMAIL}!`))
  .catch(error => console.error(error));
Running the script generates a “Forbidden (403) error

Hmmm. The red "Forbidden" text doesn't seem like a good sign. Fortunately, the yellow message is very clear:

The from address does not match a verified Sender Identity. Mail cannot be sent until this error is resolved. Visit https://sendgrid.com/docs/for-developers/sending-email/sender-identity to see the Sender Identity Requirements.

When you were setting up your SendGrid account, did you notice this persistent banner?

Screenshot of banner prompting the user to create a sender identity

Apparently it wasn't one of my better life choices to ignore this. Let's fix the problem by going to the link specified in the error message.

Since this is an individual project, you’ll follow the link that says Single Sender Verification page. If this were a business application, you might follow How to set up domain authentication.

The single sender verification documentation indicates that from your dashboard, you need to go to Settings, then Sender Authentication, and then click "Get Started" under "Verify an Address".

Fill out the form that comes up, using the email address you specified in you .env file. It's ok if you're using a free address and get a warning like this (for the purposes of this small app, it's not a problem):

Screenshot of warning about sending from a free address domain

Finally, go to the email inbox for the address that you entered and click the "Verify Single Sender" button in the email from SendGrid.

The button should take you to a page like this:

Celebratory screenshot including party horn graphic and “Sender Verified text.

Now try the script one more time:

Screenshot of command line for successful script execution

No errors! Huzzah! Check your email now (be sure to check your spam folder, as emails coming from the same address they're sent to are sometimes marked as suspicious).

If you never receive the email from the script, it might be a DMARC issue (DMARC protects an email domain from being used by outsiders for nefarious purposes). Bottom line is: you might see issues if you use an email address from a domain you don’t own. Your best bet in this case is to use domain authentication (instead of the Single Sender Verification described above) for a domain you do own.

 

Screenshot of HTML email as viewed from inbox

Fantastic! One last thing, that first table doesn't seem to be sorted properly (it's not in descending order of percent change). You can fix that by sorting the results by percentage change before making the table. Since the biggest mover belongs on top of the table, regardless of whether it's positive or negative, you'll sort on the absolute value of the changePercent:  

const generateTable = (stockData) => {
  stockData = stockData.sort((a, b) =>
        Math.abs(a.changePercent) < Math.abs(b.changePercent))

  ...

Screenshot of HTML email as viewed from inbox, with sorted table entries

Now that's a mighty fine looking email.

Conclusion

Congratulations! Now that you have completed this tutorial, you know how to:

  • Retrieve stock market data from IEX Cloud
  • Format the data into HTML tables
  • Send an HTML email with the Twilio SendGrid email API

Next Steps

Right now, the script is only being run manually. If you want this information to be sent to you automatically every day (and who wouldn't?!), you'll need to set up a scheduler like cron, Task Scheduler, or launchd.

Bonnie Schulkin is a teacher, coder, and mediocre stand-up bassist. You can find her Udemy courses at https://www.udemy.com/user/bonnie-schulkin/ and her tweets via @bonniedotdev. She’s absurdly proud to have claimed @bonnie on github and the https://bonnie.dev domain.