Advanced Email Functionality with Node.js, React.js, Nodemailer, and OAuth2 in 2023

Advanced Email Functionality with Node.js, React.js, Nodemailer, and OAuth2 in 2023

Email functionality is an essential part of most modern web applications. Sending emails directly from your app allows password reset flows, order confirmations, contact forms, and more.

In this comprehensive guide, we will walk through integrating email capabilities into a Node.js and React.js app using the following technologies:

  • Node.js - Our backend runtime

  • Express.js - Node.js web framework

  • React.js - Frontend UI library

  • Nodemailer - Module for sending emails

  • OAuth2 - For authenticating with Gmail securely

We will build an entire email-sending workflow from start to finish, including setting up a Node.js API, authenticating with Gmail via OAuth2, building a React form to capture message data, integrating Nodemailer to construct and send emails, and adding email templates/styling.

Follow this guide to learn How to Scrap LinkedIn Company Data using Voyager Api.

Overview & Goals

By the end of this guide, you will have built and deployed a production-ready email API with React and Node.js.

Some key goals:

  • Learn how to connect Node.js to Gmail using OAuth2 for authentication

  • Create reusable email templates with HTML/CSS

  • Build a React form to capture user data for emails

  • Use Nodemailer to construct email messages

  • Add email scheduling/queueing capabilities

  • Deploy the API and frontend on Vercel/Netlify

You should have Node.js and npm installed before starting. Some JavaScript knowledge is recommended.

Let's get started!

Developers are becoming addicted to drugs. Find out why in Tech and Drugs: The Dark Side of Tech

Setting up the Node.js Email API

First, we must create our Node.js server with Express to handle email-sending requests.

  • Install Dependencies

Create a new directory for the project:

mkdir node-email-api
cd node-email-api

Initialize npm:

npm init -y

Install dependencies:

npm install express nodemailer dotenv

  • express - web framework for Node.js

  • nodemailer - module for sending emails

  • dotenv - loads environment variables from the .env file

  • Create the Express App

Open index.js and set the initial Express app:

// index.js


const express = require('express');
const nodemailer = require('nodemailer');
require('dotenv').config();
const app = express();


const port = 3000;
app.listen(port, () => {
  console.log(`Server listening on port ${port}`);  
});

This creates the Express app, requires Nodemailer, loads environment variables, and configures the server to listen on port 3000.

  • Environment Variables

Next, we need to store our credentials securely using environment variables.

Create a .env file in the root of your project:

EMAIL_USER=yourgmailusername

EMAIL_PASS=yourgmailpassword

Replace yourgmailusername and yourgmailpassword with your actual Gmail credentials.

NOTE: For added security, you may want to use an App Password instead of your actual Gmail password.

We'll access these values using process.env.EMAIL_USER and process.env.EMAIL_PASS in our code.

Do you want to send emails without servers? Find out in this guide, SMTP.js – Send Email without a Server from the Browser.

Creating a Development SMTP Server

When developing and testing our application locally, we don't want actually to send emails to real addresses.

Instead, we'll create a fake SMTP server that captures any messages and prints them to the console instead of sending real emails.

Install nodemailer-stub-transport:

npm install nodemailer-stub-transport

Then, in index.js, let's create our dev SMTP transport:

// Create dev transport

const devTransport = {
  host: "localhost",
  port: 1025,  
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS
  }

}
const devAccount = await nodemailer.createTestAccount();
const transporter = nodemailer.createTransport(devTransport);

This configures a fake SMTP server running on port 1025 locally. Any emails will be logged to the console rather than sent.

Later, we can switch this to a live transport when deploying our app.

  • Sending Test Emails

With our dev SMTP server setup, let's test sending our first email.

In index.js, add:

// Send email

const info = await transporter.sendMail({
  from: '"App Name" <noreply@example.com>',
  to: 'test@example.com',
  subject: 'Test email',
  text: 'Hello world?',
  html: '<b>Hello world?</b>'

});
console.log('Message sent: %s', info.messageId);

This uses Nodemailer to send a test message.

If we run the server with node index.js, we should see the email contents logged in the terminal:

Server listening on port 3000
Accepted mail for delivery: Hello world?

Great! Our local dev environment is ready to start building out the API.

Connecting to Gmail with OAuth2

Right now, our API can send emails locally, but not through an actual Gmail account.

To connect securely, we'll integrate OAuth2 authentication with Gmail API.

  • Creating Gmail API Credentials

First, we need to create credentials for our app in Gmail:

  1. Go to Google Cloud Console

  2. Create a new project

  3. Enable the Gmail API for your project

  4. Under Credentials, create an OAuth Client ID

  5. Select Web Application and set the redirect URL to https://developers.google.com/oauthplayground

  6. Grab the Client ID and Client Secret - we'll add these to our .env file shortly.

  • Installing Googleapis

We need the googleapis library to integrate with Google's APIs:

npm install googleapis

  • OAuth2 Playground Setup

To simplify the OAuth2 flow, we'll use OAuth2 Playground to get our refresh token.

In OAuth Playground:

  1. Click the settings gear -> OAuth2 configuration

  2. Check "Use your own OAuth credentials."

  3. Enter your Client ID and Client Secret from the API console

  4. Close settings

This will enable your own custom credentials for the OAuth2 flow.

  • Obtaining Refresh Token

Now we can get a refresh token:

  1. In OAuth Playground, select "Gmail API v1" from the list of APIs

  2. Click Authorize APIs

  3. Grant access to your account

  4. Click "Exchange authorization code for tokens."

  5. Grab the refresh_token value

This is the key we'll use to generate access tokens to call the Gmail API.

  • Storing Credentials in .env

Update your .env file with the credentials:

EMAIL_USER=yourgmailusername 
EMAIL_PASS=yourgmailpassword


OAUTH_CLIENT_ID=xxxxxxxxxxx
OAUTH_CLIENT_SECRET=xxxxxxxxxx  
OAUTH_REFRESH_TOKEN=xxxxxxxxxx

Replace the values for OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, and OAUTH_REFRESH_TOKEN with your credentials.

Never commit this file to source control for security!

  • Implementing OAuth2 with Gmail

Now, in our code, we can use these credentials to authenticate with the Gmail API.

First, create a new file gmail.js, with the OAuth2 helper functions:


// gmail.js
const {google} = require('googleapis');
const OAuth2 = google.auth.OAuth2;

const oauth2Client = new OAuth2(
  process.env.OAUTH_CLIENT_ID, 
  process.env.OAUTH_CLIENT_SECRET, 
  "https://developers.google.com/oauthplayground"
);

oauth2Client.setCredentials({
  refresh_token: process.env.OAUTH_REFRESH_TOKEN
});

const getAccessToken = async () => {
  return new Promise((resolve, reject) => {
    oauth2Client.getAccessToken((err, token) => {
      if (err) {
        reject("Failed to create access token :( " + err);
      }
      resolve(token);
    });
  })
}

module.exports = {
  getAccessToken: getAccessToken
}

This creates an OAuth2 client instance and uses the refresh token to generate access tokens.

Inside index.js, import the getAccessToken method:

const {getAccessToken} = require('./gmail');

When we need to call the Gmail API, we can get an access token:

const accessToken = await getAccessToken();

This access token can be used to authenticate requests.

  • Creating Gmail Transporter

With OAuth2 setup, we can now create a Nodemailer transport for emailing through Gmail.

Inside index.js:

// Create live transport

const transporter = nodemailer.createTransport({

  service: 'gmail',

  auth: {

    type: 'OAuth2',

    user: process.env.EMAIL_USER,

    accessToken,

    clientId: process.env.OAUTH_CLIENT_ID,

    clientSecret: process.env.OAUTH_CLIENT_SECRET,

    refreshToken: process.env.OAUTH_REFRESH_TOKEN

  }

});

We pass the access token here to authenticate requests.

To send with Gmail, use transporter.sendMail() instead of our dev SMTP transport.

And that's it! Our API can now securely send emails through a Gmail account

party popper

Let's recap what we've built so far:

  • A Node.js server with Express

  • Environment variables for credentials

  • Local dev SMTP server for testing

  • OAuth2 authentication with the Gmail API

  • Nodemailer transport for sending mail through Gmail

Next, we'll build the API routes to handle sending emails based on frontend requests.

Building the Email API Routes

Now that we can securely send emails, we must create API endpoints that our React frontend can communicate with.

Creating Email Templates

First, let's set up some reusable email templates that we can render with different customer data.

Under templates/ create:

  • email-confirmation.html - Order confirmation template

  • password-reset.html - Password reset template

For example:

  • email-confirmation.html

      <div>
    
        <h1>Order Confirmation</h1>
        <p>Hi {{name}}, your order has been received!</p>
        <p>Order Details:</p>
        <ul>
          <li>Items: {{items}}</li>
          <li>Total: {{total}}</li>    
        </ul>
        <p>Thank you for shopping with us!</p>
    
      </div>
    

This is a simple confirmation email that we can populate with the customer's name, ordered items, total cost, etc.

  • password-reset.html

      <div>
    
        <h1>Password Reset</h1>
        <p>Hi {{name}}, here is your password reset link:</p>
        <p><a href="{{resetUrl}}">Reset Password</a></p>
        <p>Or copy paste this link into your browser:</p>
        <p>{{resetUrl}}</p>
    
      </div>
    

A template for sending password reset links.

These will be rendered with the customer's data before sending.

  • Email Route

Now, let's make an endpoint for general email sending:

routes/email.js


const router = require('express').Router();
const nodemailer = require('nodemailer');

router.post('/', async (req, res) => {

  try {

    const {to, from, subject, html} = req.body;

    const mailOptions = {
      from,
      to,
      subject, 
      html
    };

    const info = await transporter.sendMail(mailOptions);

    return res.json({ message: 'Email sent successfully' });

  } catch (error) {
    return res.status(500).json({ error: error.message }); 
  }

});

module.exports = router;

This takes the email data from the request body and forwards it to Nodemailer.

Import this route in index.js:

const emailRoutes = require('./routes/email');
// Use email routes
app.use('/email', emailRoutes);

We can now send a general email via a POST to /email.

  • Confirmation Email Route

Next, let's make an endpoint specifically for order confirmation emails:

routes/confirm.js


const router = require('express').Router();

const emailTemplate = require('../templates/email-confirmation'); 

router.post('/', async (req, res) => {

  try {

    const { email, name, items, total } = req.body; 

    const template = emailTemplate
      .replace('{{name}}', name)
      .replace('{{items}}', items)
      .replace('{{total}}', total);

    const mailOptions = {
      from: '"Company Name" <company@email.com>',
      to: email,
      subject: 'Your order confirmation',
      html: template
    };

    await transporter.sendMail(mailOptions);

    return res.json({ message: 'Order confirmation sent!' });

  } catch (error) {
    return res.status(500).json({ error: error.message });
  }

})

module.exports = router;

It loads the order confirmation template, replaces the placeholder data, and sends the rendered email.

Import this in index.js as well:

const confirmRoutes = require('./routes/confirm');
app.use('/confirm', confirmRoutes);

So when the customer completes an order, the frontend can POST to /confirm with their order data to send the confirmation email.

  • Password Reset Route

Finally, let's make an endpoint for password reset emails:

routes/reset.js


const router = require('express').Router();

const emailTemplate = require('../templates/password-reset');

router.post('/', async (req, res) => {

  try {

    const { email, name, resetUrl } = req.body;

    const template = emailTemplate
      .replace('{{name}}', name)
      .replace('{{resetUrl}}', resetUrl);

    const mailOptions = {
      from: '"Company" <company@email.com>',
      to: email,
      subject: 'Password reset request',
      html: template
    };

    await transporter.sendMail(mailOptions);

    return res.json({ message: 'Password reset email sent!' });

  } catch (error) {
    return res.status(500).json({ error: error.message });
  }

});

module.exports = router;

Import into index.js:

const resetRoutes = require('./routes/reset');
app.use('/reset', resetRoutes);

The frontend can POST to /reset with the user's email and reset link to send password reset instructions.

And that's it for our API! We now have:

  • Reusable email templates

  • Routes for sending general emails

  • Confirmation email endpoint

  • Password reset email endpoint

Next up, build the React frontend to use these API routes!

Creating the React Form

Now that our Node.js email API is finished let's build a React frontend for capturing user data to send emails.

Create a new React project:

npx create-react-app email-frontend

Replace App.js with:


import { useState } from 'react';

function App() {

  const [email, setEmail] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Send email API request
  }

  return (
    <div className="app">
      <h1>Send Email</h1>

      <form onSubmit={handleSubmit}>
        <input 
          placeholder="Email address"
          value={email}
          onChange={(e) => setEmail(e.target.value)} 
        />

        <button type="submit">
          Send Test Email
        </button>
      </form>
    </div>
  );
}

export default App;

This displays a simple form with an email input. Let's integrate with our API.

  • Sending Test Emails

First, we'll send a test email when the form is submitted:

// Send test email

const res = await fetch('/email', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    to: email,
    from: 'test@example.com', 
    subject: 'Test Email',
    html: '<p>This is a test email from the React app!</p>'  
  )

});


const data = await res.json();
console.log(data);

This makes a POST request to /email with the email contents.

After submitting the form, we should see the test email printed in the Node console.

  • Confirmation Email

Next, let's send a confirmation email on form submit:

const res = await fetch('/confirm', {
  method: 'POST',
  headers: {

    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email,
    name: 'Test User',
    items: ['Item 1', 'Item 2'], 
    total: '19.99'
  }) 
});

const data = await res.json();

This will call the /confirm endpoint, populate the template with the data, and send the confirmation message.

The user should receive the formatted confirmation email after submitting the form.

  • Password Reset Email

Finally, we can test sending a password reset email:

const resetUrl = 'https://example.com/reset';
const res = await fetch('/reset', {

  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email,
    name: 'Test User',
    resetUrl 
  })
});

const data = await res.json();

This will send the password reset instructions to the user with the reset URL included.

And that covers the basics of integrating our React form with the Nodemailer API! The user can now trigger emails on submit.

Connecting to a Database

Right now, our email templates and routes are hardcoded. For a more robust solution, let's connect to a database to make them dynamic.

We'll use MongoDB as our data store.

  • Installing Mongoose

First, install Mongoose to interact with MongoDB:

npm install mongoose

  • Creating a Database

Next, create a free MongoDB database at MongoDB Atlas.

Copy the connection URI provided.

  • Connecting to MongoDB

Inside index.js, connect to the database:

const mongoose = require('mongoose');

mongoose.connect(connectionURI, {
  useNewUrlParser: true,
  useUnifiedTopology: true  
});
const db = mongoose.connection;
db.on('error', err => {
  // error handling
});

db.once('open', () => {
  // connected
});

We're now connected to MongoDB!

Email Template Model

Next, create a Mongoose model for email templates:

const templateSchema = new mongoose.Schema({
  name: String,
  subject: String,
  body: String
});

const Template = mongoose.model('Template', templateSchema);

This defines the schema for a template.

Saving Templates

When an admin creates a new template, we can save it:

const template = new Template({
  name: 'Confirmation Email',
  subject: 'Order Confirmation',
  body: '<p>Thanks for your order!</p>' 
});
template.save();

The template is now saved to the database.

Fetching Templates

To render a template for sending, fetch it from MongoDB:

const template = await Template.findOne({ name: 'Confirmation Email' });
// render template.body

And that's the basics of integrating a database to store our email templates!

Now, we can easily manage templates without hardcoded files.

Adding Email Scheduling

Right now, emails are sent immediately when triggered from our API. To provide more flexibility, let's add the ability to schedule emails to be sent in the future.

  • Agenda Library

We'll use the Agenda library to schedule jobs (emails) in Node.js:

npm install agenda

  • Configuring Agenda

In index.js:


const Agenda = require('agenda');
const agenda = new Agenda({db: {address: connectionURI}});
agenda.define('send email', async job => {
  // send email here
});

(async function() {

  // Start scheduler
  await agenda.start();

  // Schedule email for future 
  await agenda.schedule('in 20 minutes', 'send email', {
    to: 'test@example.com',
    subject: 'Scheduled Email',
    body: 'This email was scheduled!'
  });

})();

This starts the Agenda scheduler, defines a 'send email' job, and schedules it to run in 20 minutes.

  • Queuing Emails

Rather than sending immediately, we can now queue emails:

await agenda.now('send email', {
  to, 
  subject,
  body
});

This will queue the email to be sent by the Agenda job at the specified time.

  • Recurring Emails

We can also easily schedule recurring reminder emails using cron syntax:

agenda.define('reminder', async job => {
  // send reminder email 
});
agenda.every('3 days', 'reminder', {

  to,

  subject,

  body

});

This will send the reminder email every 3 days.

And that covers the basics of adding email scheduling and queues! The API is now much more flexible and performant.

If you find this article thrilling, discover extra thrilling posts like this on Learnhub Blog; we write a lot of tech-related topics from Cloud computing to Frontend Dev, Cybersecurity, AI, and Blockchain. Take a look at How to Build Offline Web Applications.

Resource