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:
Go to Google Cloud Console
Create a new project
Enable the Gmail API for your project
Under Credentials, create an OAuth Client ID
Select Web Application and set the redirect URL to https://developers.google.com/oauthplayground
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:
Click the settings gear -> OAuth2 configuration
Check "Use your own OAuth credentials."
Enter your Client ID and Client Secret from the API console
Close settings
This will enable your own custom credentials for the OAuth2 flow.
- Obtaining Refresh Token
Now we can get a refresh token:
In OAuth Playground, select "Gmail API v1" from the list of APIs
Click Authorize APIs
Grant access to your account
Click "Exchange authorization code for tokens."
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
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.