Your MERN Blog needs to send emails for several reasons: a welcome email after registration, an email verification link, a password reset link, and optionally a notification when someone comments on a post. In Node.js, the standard library for sending email is Nodemailer โ it creates a connection to an email server using SMTP (or other transports) and delivers the message on your behalf. Understanding how email sending works in a MERN application โ the flow from your Express code to the recipient’s inbox โ is the foundation for building every email feature in this chapter.
How Email Sending Works
Your Express Application
โ
โผ
Nodemailer (Node.js library)
โ creates SMTP connection
โผ
Email Service / SMTP Server
(Gmail, SendGrid, Resend, Mailgun...)
โ delivers the message
โผ
Recipient's Email Server (e.g. Outlook, Yahoo)
โ
โผ
Recipient's Inbox
Key concepts:
SMTP โ Simple Mail Transfer Protocol, the standard protocol
for sending email between servers
Transporter โ Nodemailer's connection to an SMTP server,
reused across multiple sends
Transport โ The method of delivery (SMTP, SES, sendmail...)
Transactional email โ triggered by user actions (register, reset password)
vs bulk/marketing email (newsletters, announcements)
.env file is dangerous. If the file is accidentally committed, your account is compromised. Always use App Passwords or dedicated API keys from email services.Types of Emails in the MERN Blog
| Email Type | Trigger | Content | Time Sensitivity |
|---|---|---|---|
| Welcome | POST /api/auth/register | Greeting + verification link | Low โ decorative |
| Email verification | POST /api/auth/register | Verification link with token | Medium โ token expires in 24h |
| Password reset | POST /api/auth/forgot-password | Reset link with token | High โ token expires in 1h |
| Password changed | POST /api/auth/reset-password | Security alert | Low โ informational |
| New comment | POST /api/posts/:id/comments | Comment notification | Low โ can be delayed |
What You Need to Send an Email
// Every email requires these five things:
{
from: '"MERN Blog" <noreply@mernblog.com>', // sender display name + address
to: 'user@example.com', // recipient address
subject: 'Verify your MERN Blog account', // subject line
text: 'Click here to verify: https://...', // plain text fallback
html: '<p>Click here to verify: ...</p>', // HTML version (preferred)
}
// Emails should always include both text and html โ email clients
// that cannot render HTML fall back to the plain text version.
Security Considerations
| Practice | Why |
|---|---|
| Use environment variables for all credentials | Keep secrets out of source control |
| Use Gmail App Passwords, not your real password | Limits access scope; revocable |
| Rate-limit password reset requests | Prevents abuse / email flooding |
| Hash reset tokens before storing in MongoDB | If DB is breached, tokens are useless |
| Set short expiry on reset tokens (1 hour) | Limits window of vulnerability |
| Send emails asynchronously after the HTTP response | Slow email delivery does not block the API |
| Never reveal whether an email address is registered | Prevents email enumeration attacks |
Common Mistakes
Mistake 1 โ Blocking the HTTP response while waiting for email to send
โ Wrong โ user waits for email delivery before getting a response:
const register = asyncHandler(async (req, res) => {
const user = await User.create({ ... });
await sendEmail({ to: user.email, ... }); // blocks โ slow email servers delay response!
res.status(201).json({ success: true });
});
โ Correct โ send email after responding, fire-and-forget for non-critical emails:
const register = asyncHandler(async (req, res) => {
const user = await User.create({ ... });
res.status(201).json({ success: true }); // respond immediately
// Send email after โ failure does not affect the registration response
sendEmail({ to: user.email, ... }).catch(err => console.error('Email failed:', err));
});
Mistake 2 โ Revealing whether an email is registered during password reset
โ Wrong โ different response for known vs unknown email:
const user = await User.findOne({ email });
if (!user) throw new AppError('No account with that email', 404); // reveals registration status!
โ Correct โ always return the same response regardless:
const user = await User.findOne({ email });
// If user doesn't exist โ still return success (same message)
res.json({ success: true, message: 'If that email is registered, a reset link has been sent.' });
if (!user) return; // exit silently without sending email
// If user exists โ proceed to send the email
Mistake 3 โ Storing raw reset tokens in MongoDB
โ Wrong โ plaintext token in the database:
const token = crypto.randomBytes(32).toString('hex');
user.passwordResetToken = token; // if DB is breached, attacker can reset any password
โ Correct โ store the SHA-256 hash, send the raw token in the email:
const token = crypto.randomBytes(32).toString('hex');
user.passwordResetToken = crypto.createHash('sha256').update(token).digest('hex'); // hashed
await user.save();
sendEmail({ resetUrl: `${CLIENT_URL}/reset-password/${token}` }); // raw token in email โ
Quick Reference
| Concept | Key Point |
|---|---|
| Nodemailer role | Bridge between your Express code and an SMTP server |
| Transporter | Reusable SMTP connection object created once |
| Development testing | Use Ethereal Email โ catches emails without delivering |
| Gmail auth | App Password or OAuth2 โ never your real password |
| Token security | Store SHA-256 hash in DB, send raw token in email |
| Email timing | Respond first, send email after (fire-and-forget) |