Code an Email Newsletter Subscription System with Node Express Nodemailer
Learn how to code an email newsletter subscription system with Node, Express, and Nodemailer. Users will subscribe with their email address, verify their email address, and then receive newsletter emails. Users can also unsubscribe at any time.
Table of Contents 📖
- Project Demonstration
- What is a Newsletter Subscription System?
- Project Setup
- Creating the Email Functionality
- Express Server Setup
- Handle Adding a Subscriber
- Handle Unsubscribing a Subscriber
- Handle Subscriber Verification
- Handle Newsletter Email Sending
- Starting the Express Server
- Creating a Start Script
- Testing Each Route with cURL
- Gmail Takes Security Seriously
- Generating an App Password
Project Demonstration
Below is what we are going to build. First we subscribe a user to our newsletter using a cURL command. This triggers a verification email to be sent. When the user clicks verify they now receive all future emails. We trigger the email sending with another cURL command. Each email that is sent contains an unsubscribe link if the user no longer wishes to receive emails.
What is a Newsletter Subscription System?
Newsletter subscription systems are applications that send emails to subscribers when a new newsletter is published. The subscribers that receive the emails voluntarily signed up to the system with their email and can unsubscribe at any time. When they unsubscribe they no longer receive emails. Using a newsletter subscription system improves user engagement and increases the likelihood of users returning to a site or using the advertised product.
Project Setup
To begin, lets initiate our project as an ES6 npm project.
npm init es6 -y
Now lets install some dependencies. We will need express and nodemailer.
npm i express nodemailer
- express - a package to handle user requests
- nodemailer - a package to send emails from a Node application
Finally, lets install nodemon as a development dependency to update our code as we develop the application.
npm i nodemon -D
Creating the Email Functionality
First lets code our email functionality. All of this will be housed in a class called EmailService.
import nodemailer from 'nodemailer';
import SubscriptionService from './SubscriptionService.js';
export default class EmailService {
static TRANSPORTER = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
},
secure: true
});
}
- Import the nodemailer library to send emails.
- Import the SubscriptionService class, which we will code later. This class will handle all our subscription logic.
- Create the EmailService class.
- Create a static property called TRANSPORTER from the nodemailer createTransport function. This function returns a transporter object which is used to send emails. The values will come from environment variables set in our package.json start script.
- The host is the hostname or IP address to connect to for sending the email. For example, if the email you are using is a gmail account, it will be the smtp.gmail.com.
- The port is the port number to connect to. It defaults to 587 if secure is false and port 465 if secure is true.
- The secure property is true because we are using TLS.
- The auth object contains the username and password for the email account to send the email from.
Now that we have our transporter object setup, lets use it. First lets create a private method called sendEmail to send an email.
/**
* Sends an email
* @param {*} to
* @param {*} subject
* @param {*} text
* @returns
*/
#sendEmail(to, subject, html) {
const email = {
from: process.env.EMAIL_FROM,
to,
subject,
html
};
return EmailService.TRANSPORTER.sendMail(email);
}
- This method takes the email address to send the email to, the subject of the email, and the email body (named html) as arguments. These are used to form the email object to send.
- The from property is the email address to send the email from.
- The to property is the email address to send the email to.
- The subject property is the subject of the email.
- The html property will be the HTML body of the email.
- We then use the transporter object to send the email.
Now lets create a method to send a verification email. This method will make use of the sendEmail method that we just created.
/**
* Sends a verification email
* @param {*} emailToVerify
* @param {*} id
*/
async sendVerificationEmail(emailToVerify, id) {
const VERIFICATION_HTML = `<h1>Thank you for signing up for the WittCepter newsletter! An amazing Chrome Extension!</h1>
<p>Please click on the link below to verify your email</p>
<a href='${process.env.URL}/subscriber/verify?id=${id}&email=${emailToVerify}'>Verify your email</a>`;
await this.#sendEmail(emailToVerify, 'WittCepter - Verify Your Email', VERIFICATION_HTML);
console.log('Verification email sent to ' + emailToVerify);
}
- This method takes the email address and ID of the subscriber that we will send the verification email to.
- We then create the generic verification HTML. The HTML contains the link the verify the email address, specifically a GET request to /subscriber/verify containing the email address and ID as query parameters.
- Send the email using the private sendEmail method. We set the subject to be "WittCepter - Verify Your Email" (my amazing chrome extension) and the HTML body to be the VERIFICATION_HTML variable we created above.
- Finally we just log a message if the verification email was sent successfully.
Now lets create a method to send an email to every subscriber. We will call it sendBulkEmail.
/**
* Loops through the subscriber list and sends each email
* @param {*} subject
* @param {*} body
*/
async sendBulkEmail(subject, body) {
console.log('Sending an email to all subscribers');
await Promise.allSettled(
SubscriptionService.SUBSCRIBERS.map(s => {
const bodyWithUnsubscribe = body + `<a href='${process.env.URL}/subscriber/unsubscribe?id=${s.id}&email=${s.email}'>Unsubscribe</a>`;
return this.#sendEmail(s.email, subject, bodyWithUnsubscribe);
}));
console.log('Sent all emails');
}
- This method takes the subject and body of the email.
- We then loop through the list of subscribers and send an email to each one. Note that we use Promise.allSettled as opposed to Promise.all because we don't want to stop sending emails if one of them fails.
Express Server Setup
Next, we will set up our Express server. We will handle all our Express logic inside a class called ExpressService.
import express from 'express';
export default class ExpressService {
static PORT = process.env.PORT;
static NODE_ENV = process.env.NODE_ENV;
#app;
#subscribeRouter;
constructor() {
this.#app = express();
this.#subscribeRouter = express.Router();
}
#initialize() {
this.#app.use(express.json());
this.#app.use(express.urlencoded({extended: true}));
this.#app.use('/subscriber', this.#subscribeRouter);
}
#addLoggingMiddleware() {
this.#app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`, req.body);
console.log('VERIFIED', SubscriptionService.SUBSCRIBERS);
console.log('PENDING', SubscriptionService.PENDING_SUBSCRIBERS);
return next();
});
}
}
- Import the express library.
- Create two static properties called PORT and NODE_ENV. Their values will be environment variables defined inside the package.json start script.
- Create two private properties called #app and #subscribeRouter. These will be the Express app itself and the router for our subscription route.
- Add some global middleware to handle JSON payloads and URL encoded payloads.
- Place the subscribeRouter to handle requests to /subscriber.
- Add some logging middleware. We will log out the request method and path, as well as the body of the request. We will also log out the current list of subscribers and pending subscribers. Subscribers are verified and pending subscribers are pending verification.
Handle Adding a Subscriber
First, lets handle adding a new subscriber. This subscriber will not have been verified yet.
/**
* Adds a subscriber to the pending subscribers. The email used to register
* is then sent an email for verification.
*/
#addSubscribeRoute() {
this.#subscribeRouter.post('/add', async (req, res, next) => {
try {
const {email} = req.body;
if (!email) {
return res.status(400).send('Invalid request');
}
const subscriptionService = new SubscriptionService();
await subscriptionService.subscribe(email);
return res.status(200).send('Please verify your email');
} catch (err) {
console.error('Erorr adding subscriber', err);
return next(err);
}
});
}
- Create a route to handle POST requests to /subscriber/add.
- Extract the email from the request payload.
- If the email is missing, we will return a 400 status.
- Otherwise we will add the subscriber to the pending subscriber list. This will all be handled in a class called SubscriptionService.
- Return a 200 status code with the message "Please verify your email".
- If something went wrong we log out the error and then forward the error to Express's internal error handling middleware.
Now create this SubscriptionService class and the method to add a new subscriber.
import EmailService from './EmailService.js';
import {randomUUID} from 'crypto';
export default class SubscriptionService extends EmailService {
static SUBSCRIBERS = [];
static PENDING_SUBSCRIBERS = [];
/**
* Adds an email address to the subscribers list.
* Each user is represented by their id.
* @param {*} email
* @returns
*/
async subscribe(email) {
// First check if the user already exists
const subscriber = SubscriptionService.PENDING_SUBSCRIBERS.concat(SubscriptionService.SUBSCRIBERS).find(s => s.email === email);
if (subscriber) {
console.log('Subscriber already exists');
return;
}
console.log('Subscriber does not exist');
// Generate an ID
const id = randomUUID();
const newSubscriber = {
id,
email
};
await this.sendVerificationEmail(email, id);
SubscriptionService.PENDING_SUBSCRIBERS.push(newSubscriber);
}
}
- Import the EmailService class to handle the email logic.
- Import the randomUUID function from the crypto library. We will use this to generate a UUID for each new subscriber.
- Create the SubscriptionService class and extend the EmailService class.
- Create two static properties called SUBSCRIBERS and PENDING_SUBSCRIBERS. These will be the list of subscribers and pending subscribers (the ones who have not verified their email).
- Create a method called subscribe. This method will add an email address to the subscribers list if they have not already subscribed. Each subscriber consists of their id and their email.
- Send the verification email. This is the method we created in the EmailService class.
Handle Unsubscribing a Subscriber
Now lets focus on removing a subscriber from our subscription list. We will add the route to perform this functionality inside a method called addUnsubscribeRoute. This route will be called from a link inside a subscriber's email.
/**
* Add a route to unsubscribe a user from the email list
*/
#addUnsubscribeRoute() {
this.#subscribeRouter.get('/unsubscribe', async (req, res, next) => {
try {
const {id, email} = req.query;
if (!id || !email) {
return res.status(400).send('Invalid request');
}
const subscriptionService = new SubscriptionService();
subscriptionService.unsubscribe(id, email);
return res.status(200).send('Unsubscribed');
} catch (err) {
console.error('Erorr unsubscribing', err);
return next(err);
}
});
}
- Add a route to handle POST requests to /subscriber/unsubscribe. The preceding /subscriber will come from our subscribeRouter.
- Extract the id and email from the request query parameters.
- If the id or email is missing, we will return a 400 status.
- Otherwise we will remove the subscriber from the pending subscriber list using the unsubscribe method in our SubscriptionService class.
- If everything went well, we will return a 200 status code with the message "Unsubscribed".
Lets create this unsubscribe method.
/**
* Unsubscribe a subscriber from the email list
* @param {*} id
* @param {*} email
*/
unsubscribe(id, email) {
const subsciber = SubscriptionService.SUBSCRIBERS.find(s => (s.id === id && s.email === email));
if (!subsciber) {
console.log('Subscriber not unsubscribed', id, email);
} else {
console.log('Subscriber unsubscribed', id, email);
SubscriptionService.SUBSCRIBERS = SubscriptionService.SUBSCRIBERS.filter(s => s.id !== id);
}
}
- First we find the subscriber in the list of subscribers by their id and email.
- If the subscriber is not found we log out an error.
- Otherwise we remove the subscriber from the list. Now they won't receive an email when the sendBulkEmail method is called.
Handle Subscriber Verification
Now lets focus on verifying a subscriber's email. This is so we can't get spammed with lots of emails. We will add this route with a private method called addVerifyRoute.
/**
* Add a route to verify the user's email
*/
#addVerifyRoute() {
this.#subscribeRouter.get('/verify', async (req, res, next) => {
try {
const {id, email} = req.query;
if (!id || !email) {
return res.status(400).send('Invalid request');
}
const subscriptionService = new SubscriptionService();
await subscriptionService.verifySubscriber(id, email);
return res.status(200).send('Verified');
} catch (err) {
console.error('Erorr verifying', err);
return next(err);
}
});
}
- Add a route to handle GET requests to /subscriber/verify. The preceding /subscriber will come from our subscribeRouter.
- Extract the id and email from the request query parameters.
- If the id or email is missing, we will return a 400 status.
- Otherwise we will verify the subscriber using the verifySubscriber method in our SubscriptionService class.
- If everything went well, we will return a 200 status code with the message "Verified".
- If anything goes wrong during this process we will log the error and forward it to Express's internal error handling middleware.
Lets create this verifySubscriber method in our SubscriptionService class.
/**
* Verify a subscriber by their ID. If the ID is present inside the PENDING_SUBSCRIBERS
* then the subscriber is added to the SUBSCRIBERS list.
* @param {*} id
* @param {*} email
* @returns
*/
verifySubscriber(id, email) {
const subsciber = SubscriptionService.PENDING_SUBSCRIBERS.find(s => (s.id === id && s.email === email));
if (!subsciber) {
console.log('Subscriber not verified', id, email);
} else {
console.log('Subscriber verified', id, email);
SubscriptionService.SUBSCRIBERS.push(subsciber);
SubscriptionService.PENDING_SUBSCRIBERS = SubscriptionService.PENDING_SUBSCRIBERS.filter(s => (s.id !== id && s.email !== email));
}
return subsciber;
}
- First we find the subscriber in the list of pending subscribers by their id and email.
- If the subscriber is not found we log out an error.
- Otherwise we add the subscriber to the list of subscribers and remove them from the list of pending subscribers. Now they will receive an email when the sendBulkEmail method is called.
- Finally we return the subscriber which will either be undefined or the found subscriber.
Handle Newsletter Email Sending
Now lets focus on sending emails out to our subscribers. We will also do this with an Express route. We will add this route with a private method called addSendEmailRoute.
/**
* Add a route to send an email to all the subscribers
*/
#addSendEmailRoute() {
this.#subscribeRouter.post('/send', async (req, res, next) => {
try {
const {body, subject, password} = req.body;
if (!body || !subject || !password) {
return res.status(404).send('Not found');
}
if (password !== process.env.PASSWORD) {
return res.status(404).send('Not found');
}
const emailService = new EmailService();
await emailService.sendBulkEmail(subject, body);
return res.status(200).send('Emails sent to subscribers');
} catch (err) {
console.error('Erorr sending email', err);
return next(err);
}
});
}
- Create a route to handle POST requests to /subscriber/send. The /subscriber part comes from our subscribeRouter.
- Extract the body, subject and password from the request body. The body and subject are the HTML body and subject of the email respectively. The password is used to protect this route from anyone using it to send emails to our subscribers.
- If the body, subject or password is missing, we will return a 404 status. This is just to hide the route and make it look like it doesn't exist.
- If the password is incorrect we will return a 404 status.
- Otherwise we will send an email using the sendBulkEmail method in our EmailService class.
- If everything went well, we will return a 200 status code with the message "Emails sent to subscribers".
- If anything goes wrong during this process we will log the error and forward it to Express's internal error handling middleware.
Starting the Express Server
Now we just need to boostrap our Express server with all our middleware and then start it. We will do this inside a method called start.
start() {
this.#initialize();
this.#addLoggingMiddleware();
this.#addSubscribeRoute();
this.#addUnsubscribeRoute();
this.#addVerifyRoute();
this.#addSendEmailRoute();
this.#app.listen(ExpressService.PORT, () => {
console.log(`Server running on port ${ExpressService.PORT}`);
});
}
- Add some global middleware.
- Add the logging global middleware.
- Add the subscriber handling routes.
- Start the server on the provided port.
Now lets instantiate our Express server and start it inside our main server.js file.
import ExpressService from './services/ExpressService.js';
new ExpressService().start();
Creating a Start Script
Now lets create a script inside package.json to start the server with the required environment variables.
"scripts": {
"start": "PASSWORD=password EMAIL_PORT=465 EMAIL_HOST=smtp.gmail.com EMAIL_USER=YOUR_EMAIL EMAIL_PASS=YOUR_PASSWORD PORT=5001 NODE_ENV=development nodemon ."
}
Each of the arguments in this command are the ENV variables that are used throughout the application. Remember that you will have to change some of these variables to match your email, host, password, etc.
To run the application we simply need to run npm start.
npm start
[nodemon] restarting due to changes...
[nodemon] starting `node .`
Server running on port 5001
Testing Each Route with cURL
Now lets test our application using cURL. First lets add a subscriber to our email list.
curl -X POST -H "Content-Type: application/json" -d '{"email": "wittcode@gmail.com"}' localhost:5001/subscriber/add
Please verify your email
Now we will have a verification email in our inbox containing a link to verify. We can click it or also send it as a cURL like so.
curl "http://localhost:5001/subscriber/verify?id=58583616-1e9a-4bc5-8d1b-acbabe44d4e4&email=wittcode@gmail.com"
Verified
Now that we are verified lets send out a bulk email by using cURL.
curl -X POST -H "Content-Type: application/json" -d '{"password": "password", "subject": "My first email!", "body": "<h1>My first Email!</h1>"}' localhost:5001/subscriber/send
Emails sent to subscribers
Now we should see an email in our inbox. Finally, lets use a cURL to unsubscribe.
curl "http://localhost:5001/subscriber/unsubscribe?id=58583616-1e9a-4bc5-8d1b-acbabe44d4e4&email=wittcode@gmail.com"
Unsubscribed
Gmail Takes Security Seriously
If you are using Gmail to send the email, you will need to do an extra step. Sending an email with a Gmail account using nodemailer isn't as easy as other email providers as Google doesn't allow you to use a regular password for 3rd party applications. Instead, we have to generate an app password for our Gmail account. An app password is a 16 digit passcode that gives less secure apps permission to access your Google account.
Generating an App Password
We can generate an app password in our Google account settings. Note that an app password can only be generated for accounts that have two factor authentication enabled. So if you are using an account without two factor authentication, you will not be able to generate an app password. If you do have two factor authentication enabled, follow these steps.
- Navigate to your Google account.
- Click on "Security".
- Click on "2-Step Verification".
- Scroll to the bottom and click on "App passwords".
- Create a an app name and click on "Create".
- After clicking "Create" an app password should be displayed on the screen. Copy the code and save it somewhere safe as if it is lost you will have to generate a new one.
Now, in the nodemailer configuration, paste the app code in as the app password.
nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
auth: {
user: process.env.EMAIL_USER,
pass: 'PLACE_APP_PASSWORD_HERE'
},
secure: true
});
}