Send transactional emails from your API with Nodemailer — welcome emails, password resets, and notifications.
Every web application needs to send emails. Think about the last time you signed up for a new service — you received a welcome email. When you forgot your password, you clicked "Forgot Password" and received an email with a reset link. When you made a purchase, you got an order confirmation. When someone commented on your post, you got a notification digest. These are all transactional emails — emails triggered by specific user actions or system events.
Transactional emails are fundamentally different from marketing emails (newsletters, promotions, announcements). Marketing emails are sent in bulk to a mailing list at scheduled times. Transactional emails are sent one-at-a-time, in real-time, triggered by your backend code. They are critical to your application's functionality — a user literally cannot reset their password without the reset email, and they cannot verify their account without the verification email.
Your backend sends these emails programmatically through an email service or SMTP server. The flow is straightforward: a user action triggers an API endpoint (e.g., POST /forgot-password), your backend generates the email content (subject, HTML body, recipient), and it hands the email off to an email delivery service that handles the actual sending.
The key challenge with email is deliverability — making sure your emails actually reach the user's inbox instead of their spam folder. Email providers like Gmail, Outlook, and Yahoo use sophisticated spam filters that evaluate the sender's reputation, SPF/DKIM/DMARC records, email content, and sending patterns. This is why production applications use dedicated email services (SendGrid, Postmark, Amazon SES) rather than sending emails directly from their own servers — these services have established sender reputations and handle the complex infrastructure needed for reliable delivery.
Common types of transactional emails include: welcome/onboarding emails, email verification links, password reset tokens, order confirmations and receipts, shipping notifications, invoice and billing emails, security alerts (new login detected), and notification digests (weekly summary of activity).
Nodemailer is the most popular email library for Node.js, with over 14 million weekly downloads. It handles the complexity of the SMTP protocol and provides a clean API for sending emails. Install it with npm install nodemailer.
Nodemailer works by creating a transporter — an object configured with your email service credentials. The transporter knows how to connect to your SMTP server and send emails through it. You create it once when your app starts, and reuse it for every email you send.
Nodemailer supports multiple transport methods: SMTP (the standard email protocol — works with any email provider), Gmail (using OAuth2 or app-specific passwords), Outlook/Hotmail, and commercial services like SendGrid, Mailgun, Amazon SES, Postmark, and SparkPost. Each service has its own SMTP settings (host, port, authentication), but the Nodemailer API is the same regardless of which service you use.
For development, you should NEVER send real emails. Instead, use fake SMTP servers that catch emails without delivering them:
nodemailer.createTestAccount() generates credentials automatically.For production, use a dedicated email delivery service. Do NOT use Gmail or your own SMTP server — Gmail has strict sending limits (500 emails/day), and self-hosted SMTP servers have poor deliverability because they lack established sender reputations. Production services to consider:
All these services provide SMTP credentials that you plug into Nodemailer, plus REST APIs as an alternative to SMTP.
const nodemailer = require('nodemailer');
// ── Create Transporter ─────────────────────────────────
// Use environment variables for credentials (never hardcode!)
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, // e.g., 'smtp.sendgrid.net'
port: process.env.SMTP_PORT || 587, // 587 for TLS, 465 for SSL
secure: false, // true for port 465, false for 587
auth: {
user: process.env.SMTP_USER, // e.g., 'apikey' for SendGrid
pass: process.env.SMTP_PASS, // Your API key or password
},
});
// ── Generic Send Email Function ────────────────────────
async function sendEmail({ to, subject, html, text }) {
const mailOptions = {
from: '"MyApp" <noreply@myapp.com>', // Sender name + address
to, // Recipient(s)
subject, // Email subject line
html, // HTML body (rich email)
text, // Plain text fallback
};
const info = await transporter.sendMail(mailOptions);
console.log('Email sent:', info.messageId);
return info;
}
// ── Welcome Email ──────────────────────────────────────
async function sendWelcomeEmail(user) {
await sendEmail({
to: user.email,
subject: 'Welcome to MyApp!',
html: `
<h1>Welcome, ${user.name}!</h1>
<p>Thanks for joining MyApp. Here's what you can do next:</p>
<ul>
<li>Complete your profile</li>
<li>Explore the dashboard</li>
<li>Invite your team</li>
</ul>
<a href="https://myapp.com/dashboard">Go to Dashboard</a>
`,
text: `Welcome, ${user.name}! Thanks for joining MyApp.`,
});
}
// ── Password Reset Email ───────────────────────────────
async function sendPasswordResetEmail(user, resetToken) {
const resetUrl = `https://myapp.com/reset-password?token=${resetToken}`;
await sendEmail({
to: user.email,
subject: 'Reset Your Password',
html: `
<h2>Password Reset Request</h2>
<p>You requested a password reset. Click the link below:</p>
<a href="${resetUrl}">Reset Password</a>
<p>This link expires in 10 minutes.</p>
<p>If you didn't request this, ignore this email.</p>
`,
text: `Reset your password: ${resetUrl}`,
});
}Hardcoding HTML strings inside your JavaScript functions is messy and unmaintainable. As your application grows, you will have dozens of email types, each with their own layout, styling, and dynamic content. The solution is email templates — reusable HTML files with placeholders for dynamic data.
There are several approaches to email templating:
Template Engines (Handlebars, EJS, Pug) — The traditional approach. Create .hbs or .ejs files with placeholder syntax like {{user.name}} or <%= user.name %>. Load and compile the template at runtime, inject data, and get back an HTML string. Handlebars is the most popular choice for emails because its {{variable}} syntax is clean and it supports partials (reusable template fragments like headers and footers).
React Email — A modern approach where you write email templates as React components. You get component reusability, TypeScript type-checking, and a familiar development experience. The @react-email/render function converts your React component to an HTML string that you pass to Nodemailer. This is increasingly popular in the Node.js ecosystem.
MJML (Mailjet Markup Language) — A markup language specifically designed for responsive emails. It compiles to HTML that works across all email clients. Solves the biggest pain point of email HTML: cross-client compatibility.
HTML emails have notoriously quirky CSS support. Unlike web browsers, email clients have wildly inconsistent rendering engines. Outlook uses Microsoft Word's rendering engine (yes, seriously). Gmail strips <style> tags and most CSS classes. Here are the rules for email HTML:
style="color: #333; font-size: 16px;" on every element. Many email clients strip <style> blocks.Organize your templates in a dedicated folder: emails/welcome.html, emails/reset-password.html, emails/invoice.html. Create a shared layout with your brand header and footer, and compose specific emails on top of it.
The password reset flow is one of the most critical email-driven features in any application. It must be both secure and user-friendly. Here is the complete flow, step by step:
Step 1: User Requests Reset — The user clicks "Forgot Password" on your login page, enters their email address, and submits. Your frontend sends a POST request to /api/forgot-password with the email.
Step 2: Backend Generates Token — Your API receives the email and looks up the user. If the user exists, it generates a cryptographically secure random token using crypto.randomBytes(32).toString('hex'). This produces a 64-character hex string that is practically impossible to guess. The API then stores a hashed version of this token in the database (never store the raw token — if your database is compromised, attackers could use raw tokens to reset anyone's password). Also store an expiry timestamp, typically 10-15 minutes from now.
const crypto = require('crypto');
const resetToken = crypto.randomBytes(32).toString('hex');
const hashedToken = crypto.createHash('sha256').update(resetToken).digest('hex');
user.resetToken = hashedToken;
user.resetExpiry = Date.now() + 10 * 60 * 1000; // 10 minutes
await user.save();Step 3: Send Reset Email — The API sends an email containing a link with the raw (unhashed) token: https://myapp.com/reset-password?token=abc123.... The user receives this email and clicks the link.
Step 4: User Submits New Password — The link takes the user to a reset form on your frontend. They enter their new password. The frontend sends a POST request to /api/reset-password with the token and the new password.
Step 5: Backend Verifies & Updates — The API hashes the incoming token and searches the database for a user with a matching hashed token that has NOT expired. If found: hash the new password with bcrypt, update the user's password, clear the reset token and expiry fields, and respond with success. If not found or expired: respond with an error.
Critical Security Rules:
Why should you use a service like SendGrid instead of your own SMTP server for production?