For a long time, I thought sending emails from a Node.js project would be simple.
Install Nodemailer, write a few lines, done.
Not really.
While adding email sending functionality to my project, I ran into configuration issues, especially with dotenv, Gmail authentication, and OAuth2. After some debugging, I finally got it working.
So in this post, I want to explain how to set up Nodemailer with Gmail OAuth2 in a simple way, and also share the exact problems I faced and how I fixed them.
Why I used OAuth2 instead of password login
Earlier, many people used Gmail with just email and password. But that is not the recommended way now.
A better setup is:
- Gmail API enabled
- OAuth2 credentials from Google Cloud
- Refresh token from OAuth Playground
- Nodemailer configured with those values
That is also the method used in the guide I followed.
Step 1: Install packages
npm install nodemailer dotenv
Step 2: Create OAuth2 credentials
Go to Google Cloud Console and do these things:
- Create a project
- Enable Gmail API
- Create OAuth 2.0 Client ID
- Choose Web Application
- Add redirect URI:
http://localhosthttps://developers.google.com/oauthplayground
After that, you will get:
CLIENT_IDCLIENT_SECRET
Step 3: Generate refresh token
Open OAuth 2.0 Playground and:
- Click the settings icon
- Enable Use your own OAuth credentials
- Paste your
CLIENT_IDandCLIENT_SECRET - Set access type to Offline
- Select the Gmail scope:
https://mail.google.com/
- Authorize APIs
- Exchange authorization code for tokens
Now copy the refresh token.
Step 4: Create your .env file
GOOGLE_USER=yourgmail@gmail.com
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
GOOGLE_REFRESH_TOKEN=your_refresh_token
Make sure the variable names match exactly with your code.
Step 5: Configure Nodemailer
Here is a clean setup:
import dotenv from "dotenv";
import nodemailer from "nodemailer";
dotenv.config();
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
type: "OAuth2",
user: process.env.GOOGLE_USER,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
},
});
// Verify the connection configuration
transporter.verify((error, success) => {
if (error) {
throw new Error(`Error connecting to email server: ${error.message}`);
} else {
console.log("Email server is ready to send messages");
}
});
// Function to send email
export const sendEmail = async ({ to, subject, text, html }) => {
try {
const info = await transporter.sendMail({
from: `"Your Name" <${process.env.Gmail_User}>`, // sender address
to, // list of receivers
subject, // Subject line
text, // plain text body
html, // html body
});
console.log('Message sent: %s', info.messageId);
console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info));
} catch (error) {
throw new Error(`Error sending email: ${error.message}`);
}
};
Step 6: Use it in your project
await sendEmail({
to: "recipient@example.com",
subject: "Test Mail",
text: "This is a test email",
html: "<p>This is a test email</p>",
});
If everything is correct, your mail should be sent successfully.
What I learned
Setting up Nodemailer is not just about writing sendMail().
You also need to understand:
- how Gmail authentication works
- why OAuth2 is needed
- why environment variables must load properly
- why even small config mistakes can break everything
This was one of those features that looked small from outside, but taught me a lot while implementing it.
Problems I faced and how I solved them
1. My dotenv configuration was done afterwards
This was my main mistake.
I had set up the email functionality, but the environment variables were not being loaded properly at the right time. Because of that, values like client ID, client secret, refresh token, or email user were not available when Nodemailer needed them.
Fix
I made sure to call:
dotenv.config();
at the top, before using process.env values.
That small order issue was enough to break the whole setup.
2. Gmail authentication error
I got an error like:
530-5.7.0 Authentication Required
This happened because Gmail was not accepting the request as properly authenticated.
Fix
I stopped treating it like a simple email-password login and used the correct OAuth2 setup:
- Gmail API enabled
- correct client ID
- correct client secret
- correct refresh token
- correct Gmail account in
GOOGLE_USER - add same Gmail account as a test user in Google Cloud Console
3. Following the guide was not enough without matching my own project structure
The guide I followed was helpful, but I still had to adjust it to my own project setup.
For example:
- I was using my own environment variable names
- my project used
importsyntax - my file structure was different from the guide
Fix
I kept the logic the same, but adapted the code to my own project.
That made me realize an important thing:
Tutorials and README files give the path, but you still need to fit that path into your own project properly.
4. Small config mistakes can waste a lot of time
Even when the code looks correct, things can still fail because of:
- wrong variable names
- wrong refresh token
- wrong Google account
- missing
.env -
dotenvloading too late
Fix
I checked everything one by one instead of changing random things.
That made debugging much easier.
Final thoughts
This feature looked small, but it taught me a very useful lesson:
Backend development is not only about writing logic. A lot of real work is in configuration, authentication, environment setup, and debugging.
Getting Nodemailer working with Gmail OAuth2 was frustrating at first, but once it worked, I understood the setup much better.
And honestly, that is the good part of building projects:
you do not just learn the feature, you learn the mistakes around the feature too.
Top comments (0)