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.

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:

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: