← All articles
INFRASTRUCTURE Email for Developers: Transactional Email, SMTP, Aut... 2026-02-09 · 7 min read · email · smtp · ses

Email for Developers: Transactional Email, SMTP, Authentication, and Local Testing

Infrastructure 2026-02-09 · 7 min read email smtp ses postmark transactional

Email for Developers: Transactional Email, SMTP, Authentication, and Local Testing

Email is deceptively hard. Sending a password reset email sounds simple until you discover that Gmail silently drops your messages, your domain has no SPF record, and your "from" address is flagged as suspicious. This guide covers everything a developer needs to know about transactional email: picking a provider, configuring authentication, building reliable sending, and testing locally without spamming real inboxes.

Transactional vs Marketing Email

Transactional email is triggered by user actions: password resets, order confirmations, account notifications, two-factor codes. These emails are expected by the recipient and need to arrive quickly and reliably.

Marketing email is sent in bulk to lists: newsletters, promotions, announcements. These have different deliverability rules, legal requirements (CAN-SPAM, GDPR), and typically use different services.

This guide focuses on transactional email. If you're sending newsletters, use a dedicated marketing platform like Mailchimp, ConvertKit, or Buttondown.

Provider Comparison

Feature Postmark Amazon SES Resend SendGrid
Pricing $1.25/1K emails $0.10/1K emails Free tier: 3K/mo, then $20/mo Free tier: 100/day, then $20/mo
Deliverability Excellent (dedicated IPs included) Good (requires warm-up) Good Good (dedicated IPs extra)
API quality Excellent Functional Excellent (modern) Adequate
SMTP support Yes Yes Yes Yes
Webhooks Delivery, bounce, spam complaints SNS notifications Delivery, bounce, complaints Delivery, bounce, open, click
Templates Server-side (Mustachio) SES templates React Email Handlebars
Setup complexity Low Medium (AWS config) Low Low
Best for Apps that need high deliverability High volume, cost-sensitive Modern dev experience Established apps, high volume

Postmark

Postmark focuses exclusively on transactional email and has the best deliverability reputation. They actively refuse marketing email, which means their IP addresses aren't polluted by bulk senders.

import { ServerClient } from "postmark";

const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN);

await client.sendEmail({
  From: "[email protected]",
  To: "[email protected]",
  Subject: "Your password reset link",
  HtmlBody: "<p>Click <a href='https://myapp.com/reset?token=abc'>here</a> to reset your password.</p>",
  TextBody: "Visit https://myapp.com/reset?token=abc to reset your password.",
  MessageStream: "outbound",
});

Amazon SES

SES is the cheapest option at scale ($0.10 per 1,000 emails). The tradeoff is more setup work: you start in sandbox mode (can only send to verified addresses), need to request production access, and should warm up your sending volume gradually.

import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";

const ses = new SESv2Client({ region: "us-east-1" });

await ses.send(new SendEmailCommand({
  FromEmailAddress: "[email protected]",
  Destination: {
    ToAddresses: ["[email protected]"],
  },
  Content: {
    Simple: {
      Subject: { Data: "Your password reset link" },
      Body: {
        Html: { Data: "<p>Click <a href='https://myapp.com/reset?token=abc'>here</a> to reset.</p>" },
        Text: { Data: "Visit https://myapp.com/reset?token=abc to reset your password." },
      },
    },
  },
}));

Resend

Resend is a newer provider with an excellent developer experience. Their standout feature is React Email integration, which lets you build email templates as React components.

import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

await resend.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Your password reset link",
  html: "<p>Click <a href='https://myapp.com/reset?token=abc'>here</a> to reset your password.</p>",
});

SendGrid

SendGrid is the oldest major player and handles enormous volumes. The API is functional but shows its age compared to Resend or Postmark.

import sgMail from "@sendgrid/mail";

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

await sgMail.send({
  to: "[email protected]",
  from: "[email protected]",
  subject: "Your password reset link",
  text: "Visit https://myapp.com/reset?token=abc to reset your password.",
  html: "<p>Click <a href='https://myapp.com/reset?token=abc'>here</a> to reset your password.</p>",
});

Email Authentication: SPF, DKIM, and DMARC

If you skip email authentication, your messages will land in spam. These three DNS records tell receiving mail servers that your emails are legitimate.

SPF (Sender Policy Framework)

SPF declares which servers are allowed to send email for your domain. It's a TXT record on your domain.

# DNS TXT record for myapp.com
v=spf1 include:spf.postmarkapp.com include:amazonses.com ~all

This says: "Postmark and SES are authorized to send email for myapp.com. Soft-fail everything else."

Common mistake: Having multiple SPF records. You can only have one SPF TXT record per domain. Combine multiple providers into a single record.

DKIM (DomainKeys Identified Mail)

DKIM adds a cryptographic signature to your emails. The receiving server verifies it against a public key in your DNS. Your email provider gives you the DKIM records to add.

# DNS TXT record (example from Postmark)
# Host: 20240101._domainkey.myapp.com
# Value:
k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ...

Every provider has different DKIM setup instructions, but the process is always: add the TXT (or CNAME) records they give you, then verify in their dashboard.

DMARC (Domain-based Message Authentication, Reporting, and Conformance)

DMARC tells receiving servers what to do when SPF or DKIM checks fail, and where to send reports about authentication failures.

# DNS TXT record for _dmarc.myapp.com
v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100

Recommended rollout: Start with p=none to collect reports without affecting delivery. Review the reports for a few weeks to make sure legitimate email isn't failing authentication. Then move to p=quarantine and eventually p=reject.

Verifying Your Setup

# Check SPF
dig TXT myapp.com | grep spf

# Check DKIM (replace selector with your provider's selector)
dig TXT 20240101._domainkey.myapp.com

# Check DMARC
dig TXT _dmarc.myapp.com

Use mail-tester.com to send a test email and get a deliverability score. It checks SPF, DKIM, DMARC, content, and blacklists.

SMTP: When You Need It

Most modern providers offer both an HTTP API and SMTP. Use the HTTP API when you can (it's simpler, faster, and gives you better error handling). Use SMTP when:

// SMTP with Nodemailer (works with any provider)
import nodemailer from "nodemailer";

const transport = nodemailer.createTransport({
  host: "smtp.postmarkapp.com",
  port: 587,
  secure: false,
  auth: {
    user: process.env.POSTMARK_SERVER_TOKEN,
    pass: process.env.POSTMARK_SERVER_TOKEN,
  },
});

await transport.sendMail({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Your password reset link",
  text: "Visit https://myapp.com/reset?token=abc to reset your password.",
  html: "<p>Click <a href='https://myapp.com/reset?token=abc'>here</a> to reset.</p>",
});

Local Testing with Mailpit

Never send real emails during development. Use a local SMTP server that captures all outgoing email and displays it in a web UI.

Mailpit (Recommended)

Mailpit is the modern replacement for MailHog. It's a single binary, fast, and has a clean web UI with search and API access.

# docker-compose.yml
services:
  mailpit:
    image: axllent/mailpit
    ports:
      - "8025:8025"  # Web UI
      - "1025:1025"  # SMTP
    environment:
      MP_MAX_MESSAGES: 5000
      MP_SMTP_AUTH_ACCEPT_ANY: 1

Configure your application to use Mailpit's SMTP in development:

// config/email.ts
const emailConfig = {
  development: {
    host: "localhost",
    port: 1025,
    secure: false,
    // No auth needed
  },
  production: {
    host: "smtp.postmarkapp.com",
    port: 587,
    secure: false,
    auth: {
      user: process.env.POSTMARK_SERVER_TOKEN,
      pass: process.env.POSTMARK_SERVER_TOKEN,
    },
  },
};

Open http://localhost:8025 to see every email your app sends during development. Mailpit also has an API for automated testing:

// In your test suite: verify an email was sent
const response = await fetch("http://localhost:8025/api/v1/messages");
const { messages } = await response.json();
const resetEmail = messages.find(
  (m) => m.Subject === "Your password reset link"
);

expect(resetEmail).toBeDefined();
expect(resetEmail.To[0].Address).toBe("[email protected]");

MailHog (Legacy Alternative)

MailHog is the predecessor to Mailpit. It still works but is no longer actively maintained.

services:
  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"  # Web UI
      - "1025:1025"  # SMTP

Building Reliable Email Sending

Always Send Asynchronously

Email sending should never block your request/response cycle. Use a background job queue.

// Don't do this in a request handler
app.post("/api/auth/forgot-password", async (req, res) => {
  const token = await createResetToken(req.body.email);
  // BAD: If email sending fails or is slow, the user waits
  await sendResetEmail(req.body.email, token);
  res.json({ success: true });
});

// Do this instead
app.post("/api/auth/forgot-password", async (req, res) => {
  const token = await createResetToken(req.body.email);
  // GOOD: Queue the email and respond immediately
  await jobQueue.add("send-email", {
    template: "password-reset",
    to: req.body.email,
    data: { token },
  });
  res.json({ success: true });
});

Handle Bounces and Complaints

Set up webhooks from your email provider to handle bounces (invalid addresses) and spam complaints. Continuing to send to bounced addresses damages your sender reputation.

// Webhook handler for Postmark
app.post("/webhooks/postmark", async (req, res) => {
  const event = req.body;

  switch (event.RecordType) {
    case "Bounce":
      await markEmailInvalid(event.Email, "bounced");
      break;
    case "SpamComplaint":
      await markEmailInvalid(event.Email, "complained");
      break;
  }

  res.sendStatus(200);
});

Always Send Both HTML and Plain Text

Some email clients don't render HTML, and spam filters look more favorably on emails that include a plain text alternative. Always include both.

Picking a Provider

Choose Postmark if deliverability is your top priority and you're willing to pay a premium. Their transactional-only policy means their IP reputation stays clean.

Choose SES if you're already on AWS and sending high volumes where cost matters. Be prepared for more setup work and IP warm-up.

Choose Resend if you want the best developer experience and are building with React. Their API is clean and modern.

Choose SendGrid if you need a proven platform that handles both transactional and marketing email, or if you're already using it.

For most new projects, start with Resend or Postmark. Their APIs are a pleasure to work with, setup takes minutes, and you can switch later if your needs change. The most important thing is to get SPF, DKIM, and DMARC configured correctly from day one. Retroactively fixing deliverability is much harder than setting it up right the first time.