Advanced20 min read

Sending Emails

Send transactional emails from your API with Nodemailer — welcome emails, password resets, and notifications.

Why Send Emails from Your API?

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 Setup

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:

  • Ethereal (ethereal.email) — Created by the Nodemailer team. Generates disposable test accounts instantly. Emails are captured and viewable in a web interface. Nodemailer has built-in support: nodemailer.createTestAccount() generates credentials automatically.
  • Mailtrap (mailtrap.io) — A more feature-rich option with a visual inbox, HTML preview, spam analysis, and team collaboration. Free tier available.
  • MailHog — A self-hosted option you can run locally with Docker. No external dependencies.

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:

  • SendGrid — Most popular. Generous free tier (100 emails/day). Excellent API and analytics.
  • Postmark — Focused on transactional email. Known for fast delivery and high inbox rates.
  • Amazon SES — Cheapest at scale ($0.10 per 1,000 emails). Requires more setup.
  • Resend — Modern developer-focused service with a clean API and React Email integration.

All these services provide SMTP credentials that you plug into Nodemailer, plus REST APIs as an alternative to SMTP.

Complete Email Sending Setup

javascript
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}`,
  });
}

Email Templates

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:

  • Use inline stylesstyle="color: #333; font-size: 16px;" on every element. Many email clients strip <style> blocks.
  • Use tables for layout — Flexbox and CSS Grid do not work in most email clients. Tables are the only reliable layout mechanism.
  • Avoid background images — Many clients block them by default.
  • Test across clients — Use tools like Litmus or Email on Acid to preview your emails in Gmail, Outlook, Apple Mail, Yahoo Mail, and mobile clients. What looks perfect in Gmail might be broken in Outlook.
  • Always include a plain text version — Some users have HTML emails disabled, and including plain text improves deliverability scores.

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.

Password Reset Flow

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.

javascript
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:

  • Never reveal if an email exists in your system. Whether the email is registered or not, always respond with the same message: "If an account with that email exists, a reset link has been sent." This prevents attackers from enumerating which emails are registered.
  • Tokens must expire quickly (10-15 minutes). A token that lives for 24 hours gives attackers a large window to intercept it.
  • Tokens are single-use. Delete or invalidate the token after it is used.
  • Rate limit the endpoint. Prevent attackers from flooding a user's inbox or brute-forcing tokens.
  • Use HTTPS everywhere. The token travels in the URL — without HTTPS, it can be intercepted.

Why should you use a service like SendGrid instead of your own SMTP server for production?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.