Nodemailer in Node.js: Send Emails with Gmail or Any SMTP Server

Sending emails programmatically is essential for user verification, password resets, and notifications in web apps. This guide covers setting up Nodemailer with Gmail's SMTP in Node.js, using HTML templates, and scaling with Amazon SES for high-volume email sending.

Nodemailer in Node.js: Send Emails with Gmail or Any SMTP Server

Sending emails programmatically is a common feature in modern web applications, whether for user verification, password resets, or notifications. In Node.js, Nodemailer is the go-to package for sending emails via any SMTP server, including Gmail, Outlook, and custom mail servers.

As your application grows, scalability becomes a concern. While Gmail or Outlook SMTP servers work for small projects, you may need more robust email solutions like Amazon SES (Simple Email Service) for high-volume email sending.

This guide covers how to configure Nodemailer, use environment variables for secure credentials, send emails with HTML templates, and scale with Amazon SES.

Setting Up Nodemailer in Node.js

To start, you'll need to install the required packages. In this example, we’ll be using dotenv to manage environment variables securely.

1. Install Nodemailer and Dotenv

In your Node.js project, install the necessary packages:

npm install nodemailer dotenv

2. Configure Environment Variables

In the root of your project, create a .env file to securely store your email credentials:

GMAIL_CLIENT_ID=your-gmail-email@gmail.com
GMAIL_APP_PASSWORD=your-app-password
NODE_ENV=development
Note: You should use an App Password generated from your Gmail account for secure authentication. Standard passwords won’t work with Gmail if 2FA is enabled.

I always use a particular directory structure for most of the medium-sized node projects. I have talked about this in the article below:

Scalable Directory Structure for NodeJS + Express Web Servers
This blog explores a scalable folder structure for Node.js Express servers using Docker, TypeScript, and domain-driven design. Learn how to organize key components like configurations, domains, middlewares, and utilities to ensure maintainability, modularity, and scalability in your application.

For the nodemailer setup, here is what I do

📂 project-root
 ┣ 📂 src          
 ┃ ┣ 📂 configs                       # Configuration files
 ┃ ┃ ┣ 📜 nodemailer.js               # Nodemailer configuration file
 ┃
 ┃ ┣ 📂 libs                      
 ┃ ┃ ┣ 📜 nodemailer_transporter.js   # Email sending logic (Nodemailer)
 ┃
 ┃ ┣ 📂 templates                     # Email HTML templates
 ┃ ┃ ┣ 📜 email_verification_template.html  
 ┃ ┃ ┣ 📜 password_reset_template.html    
 ┃
 ┣ 📜 .env                            # Environment variables
 ┣ 📜 package.json                    # Project metadata and dependencies
 ┣ 📜 README.md                       # Project documentation
 ┗ 📜 .gitignore                      # Ignoring files like .env

3. Nodemailer Configuration

We’ll use this configuration to define the email service and authentication credentials for Nodemailer. This is done by loading values from your .env file using dotenv.

// configs/nodemailer.js
import dotenv from "dotenv";
dotenv.config();

export const nodemailerConfig = {
  service: "gmail", // Using Gmail as the service
  port: 465,        // SMTP port
  secure: true,     // Secure connection (SSL)
  auth: {
    user: process.env.GMAIL_CLIENT_ID || "",
    pass: process.env.GMAIL_APP_PASSWORD || "",
  },
};

This config enables you to use Gmail’s SMTP server with SSL for secure email sending. You can easily replace the service and auth fields for other SMTP servers (like Outlook or SendGrid).

Creating the Email Transporter

Next, let's create a transporter that will use the SMTP configuration to send emails. This transporter is responsible for delivering emails via the specified SMTP server.

// services/emailService.js
import dotenv from "dotenv";
import nodemailer from "nodemailer";
import { nodemailerConfig } from "../../configs/nodemailer";

dotenv.config();

// Create a Nodemailer transporter using the configuration
const transporter = nodemailer.createTransport(nodemailerConfig);

// Verify the transporter connection
export const checkTransporterStatus = () => {
  transporter.verify(function (error, success) {
    if (error) {
      console.error("Error verifying transporter:", error);
    } else {
      console.log("Server is ready to take our messages");
    }
  });
};

By using the verify method, we ensure the SMTP server connection is ready before attempting to send emails. Running this function will help catch any potential configuration issues early on.

Sending Verification Emails with Nodemailer

In many applications, you’ll want to send verification emails for user sign-up or password reset emails. Here’s a function that sends different types of emails, based on templates for each case.

1. Organizing Email Templates

We'll use HTML templates to format our emails. The templates will be dynamically updated with values like OTP and username before sending.

// services/emailService.js

import fs from "fs";
import path from "path";
import { verificationTypes } from "../../utils/verificationTypes";

// Function to get the email template path
const getTemplatePath = (templateName) => {
  const projectRoot = process.cwd();
  const isProduction = process.env.NODE_ENV === "production";

  const templatesDir = isProduction
    ? path.join(projectRoot, "dist", "templates")
    : path.join(projectRoot, "src", "templates");

  return path.join(templatesDir, templateName);
};

// Function to send verification or password reset emails
export const sendVerificationEmail = (
  emailAddress,
  otp,
  type,
  username = "User"
) => {
  return new Promise((resolve, reject) => {
    if (!emailAddress) {
      reject("Email address is undefined or null");
      return;
    }

    // Set the appropriate email template and subject
    let mailTemplatePath = getTemplatePath("email_verification_template.html");
    
    let mailSubject = "OTP for Email Verification";

    if (type === verificationTypes.password_reset) {
      mailTemplatePath = getTemplatePath("password_reset_template.html");
      mailSubject = "OTP for Password Reset";
    }

    // Read and update the template with the dynamic values
    fs.readFile(mailTemplatePath, { encoding: "utf-8" }, (err, data) => {
      if (err) {
        console.error("Error reading email template:", err);
        reject("Failed to load email template");
        return;
      }

      // Replace placeholders in the template with real values
      const html = data
        .replace("{{otp}}", otp)
        .replace("{{username}}", username);

      // Set up the email options
      const mailOptions = {
        from: process.env.GMAIL_CLIENT_ID || "",
        to: emailAddress,
        subject: mailSubject,
        html: html,
      };

      // Send the email
      transporter.sendMail(mailOptions, (error, info) => {
        if (error) {
          console.error("Error sending email:", error);
          reject("Failed to send email");
          return;
        }
        console.log("Email sent:", info.response);
        resolve(info.response);
      });
    });
  });
};

2. HTML Templates

In your project, create a directory for your email templates. Here are two examples:

email_verification_template.html:

<html>
  <body>
    <h1>Email Verification</h1>
    <p>Hello {{username}},</p>
    <p>Your OTP for email verification is: <strong>{{otp}}</strong></p>
  </body>
</html>

password_reset_template.html:

<html>
  <body>
    <h1>Password Reset</h1>
    <p>Hello {{username}},</p>
    <p>Your OTP for password reset is: <strong>{{otp}}</strong></p>
  </body>
</html>

These templates will dynamically update with the otp and username passed into the function.

Now, you can use the method sendVerificationEmail() in our controllers to send verification mail or any other mail. The setup will be similar, you might want to create different methods for different purposes and use cases. Similarly, you can have a corresponding html template for that.

Scaling with Amazon SES for High-Volume Email Sending

While using Gmail’s SMTP server with Nodemailer is a good starting point for small-scale projects, it comes with limitations, particularly when scaling to handle a higher volume of emails. Gmail imposes a cap on the number of emails you can send:

  • 500 emails per day for regular Gmail users.
  • 2,000 emails per day for G Suite users.

For businesses and applications that need to send a high volume of emails—such as user verification emails, password reset notifications, or bulk marketing emails—this limit can become a bottleneck. At this point, switching to a more robust solution like Amazon Simple Email Service (SES) is a smart move.

Why Use Amazon SES?

Amazon SES is a highly scalable email-sending service that provides an SMTP interface, making it easy to integrate with Nodemailer. It allows you to send thousands of emails daily without worrying about hitting caps imposed by providers like Gmail. This is especially useful for applications that need to send regular email updates, marketing campaigns, or handle multiple user transactions.

Here's what Amazon SES offers:

  • In Sandbox Mode (Testing Phase):
    • You can send up to 200 emails per 24 hours.
    • Maximum send rate: 1 email per second.
    • Sandbox mode is useful for testing, and you can request a production environment for real-world email sending.
  • In Production Mode (High-Volume Sending):
    • Once approved for production, you can send up to 50,000 emails per 24 hours.
    • Maximum send rate: 14 emails per second.
    • You can also request an increase in these limits based on your needs, making it ideal for large-scale marketing campaigns.

We can integrate SES very easily in this setup, we just need to do some changes in the config of nodemailer

// Replace Gmail SMTP config with Amazon SES config
export const nodemailerConfig = {
  host: "email-smtp.us-east-1.amazonaws.com", // Amazon SES SMTP endpoint
  port: 587,                                  // TLS port
  secure: false,                              // Use TLS
  auth: {
    user: process.env.SES_ACCESS_KEY_ID,      // Your SES Access Key
    pass: process.env.SES_SECRET_ACCESS_KEY,  // Your SES Secret Key
  },
};

And just by changing the SMTP server and the credentials, you are good to go. Although there are a lot more things to do on the other side of the setup in AWS Console. But, that is for another article.

Nodemailer makes email sending in Node.js easy, whether you are using Gmail, Outlook, or scaling up to a service like Amazon SES. By leveraging environment variables, you can securely store credentials and switch between SMTP servers depending on your application's needs.

For small projects, Gmail or Outlook may be sufficient, but when you need to handle high volumes of email traffic, Amazon SES is a powerful, scalable solution. With the ability to swap configurations easily, Nodemailer ensures your email service can grow with your application.

That's it for today. See ya 👋


Further Reading: