Code Tips11 min read

SendGrid to React Email: Migrate Your SaaS Email Stack to Resend

Step-by-step guide to migrating from SendGrid to Resend and React Email. Covers DNS cutover, API swap, template rebuild strategy, suppression list export, and webhook changes.

R

React Emails Pro

April 15, 2026

On May 27, 2025, Twilio killed SendGrid's permanent free tier. New accounts get a 60-day trial, then paid plans start at $19.95/month. Thousands of SaaS founders woke up to an email telling them the thing they'd been sending password resets and welcome emails through for years now had an expiration date.

Pricing was just the trigger, though. The frustrations had been piling up for a while: a proprietary template editor you can't version control, Handlebars syntax with zero type safety, and a debugging workflow that amounts to squinting at raw HTML in a browser tab. Losing the free tier gave teams the push to finally deal with all of it.

This post walks through what that migration to Resend and React Email looks like in practice, what changes at each layer, and where teams burn the most time when they try to rebuild every template from a blank file.

We spent three weeks hand-coding 14 email templates and testing them across Outlook, Gmail, and Apple Mail. Every fix for one client broke another. Then someone found a pre-built React Email library and we shipped the rest of the migration over a weekend.

Indie SaaS dev

r/SaaS, March 2026


What actually changes in the migration

Most people assume this is an API key swap. It's not. Four layers need to change, and skipping any one of them causes problems once you're live.

LayerSendGridResend + React EmailMigration Effort
DNS / AuthenticationSPF, DKIM via SendGrid dashboardSPF, DKIM via Resend dashboard30 minutes - just new DNS records
API Integration@sendgrid/mail SDK, REST APIresend SDK, one-line sends1-2 hours per endpoint
TemplatesDynamic Templates (Handlebars in web UI)React components with TypeScript props2-5 hours per template from scratch
DeliverabilityIP warm-up via SendGridShared IPs (pre-warmed) or dedicatedZero if using shared IPs

The four layers of a SendGrid to Resend migration

The template layer is where migrations stall. DNS and API changes are mechanical. Rebuilding 10-20 email templates with proper rendering across Outlook, Gmail, and Apple Mail is the multi-week time sink that kills momentum.

The migration timeline

Based on what we've seen from teams making this switch in early 2026, a typical migration follows a predictable pattern. The first two days feel fast. Then the templates hit.

Day 1

DNS and domain verification

Add Resend's DKIM and SPF records alongside your existing SendGrid records. Both providers can coexist during the transition. Verify your domain in the Resend dashboard - this usually propagates within an hour.
Day 2

API integration swap

Replace @sendgrid/mail calls with the Resend SDK. The API surface is much smaller: one function, typed parameters, no template IDs to manage.
Days 3-5

Template rebuild (the bottleneck)

This is where most teams lose time. Each SendGrid dynamic template needs to become a React Email component with proper email-client compatibility. Building from scratch means fighting table layouts, inline styles, and Outlook conditional comments.
Day 6

Testing and cutover

Send test emails through Resend to verify rendering across clients. Once confirmed, update DNS to remove SendGrid records and route all traffic through Resend.
Day 7

Monitoring

Watch bounce rates and delivery metrics in the Resend dashboard for the first 48 hours. Shared IPs are pre-warmed, so deliverability should be stable from day one.

The shortcut most teams miss

Days 3-5 are the bottleneck. Teams that use production-ready React Email templates compress that three-day stretch into a few hours of customization instead of building from a blank file. The template layer is the only part of this migration that doesn't have to be built from scratch.


The API swap: SendGrid vs Resend

The API change is the easiest part. A welcome email send in both providers shows the difference clearly.

SendGrid: template ID + substitutions

lib/email-sendgrid.ts
import sgMail from "@sendgrid/mail";

sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

export async function sendWelcomeEmail(
  to: string,
  name: string,
  plan: string
) {
  await sgMail.send({
    to,
    from: "hello@yourapp.com",
    templateId: "d-a1b2c3d4e5f6", // opaque ID from SendGrid UI
    dynamicTemplateData: {
      name,        // {{name}} in Handlebars template
      plan,        // {{plan}} in Handlebars template
      login_url: `https://yourapp.com/login`,
    },
  });
}

The problem: that template ID is a pointer to HTML you can't see in your codebase. If someone changes the template in SendGrid's web editor, your code has no idea. No type checking, no version control, no way to test locally.

Resend + React Email: components with typed props

lib/email-resend.ts
import { Resend } from "resend";
import { WelcomeEmail } from "@/emails/welcome";

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

export async function sendWelcomeEmail(
  to: string,
  name: string,
  plan: "starter" | "pro" | "enterprise"
) {
  await resend.emails.send({
    to,
    from: "hello@yourapp.com",
    subject: `Welcome to YourApp, ${name}`,
    react: <WelcomeEmail name={name} plan={plan} />,
  });
}

The template is a React component in your repo. TypeScript catches typos at build time. You can preview it locally with email dev. It lives in version control next to the code that sends it.

Notice the plan parameter: in SendGrid, it is an untyped string that could be anything. In React Email, it is a union type that your IDE autocompletes and your compiler enforces.

The template rebuild trap

This is where migrations go sideways. You have 12 SendGrid templates that work fine. Now every single one needs to become a React Email component. The instinct is to start from scratch and hand-write each one.

emails/welcome-diy.tsx
// The DIY approach: every template is a 200+ line fight
// with table layouts and email client quirks
import {
  Html, Head, Preview, Body, Container,
  Section, Row, Column, Img, Text, Button, Hr
} from "@react-email/components";

interface WelcomeEmailProps {
  name: string;
  plan: string;
}

export function WelcomeEmail({ name, plan }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to YourApp, {name}</Preview>
      <Body style={{ backgroundColor: "#f6f9fc", fontFamily: "..." }}>
        <Container style={{ /* 30+ inline style properties */ }}>
          {/* Header with logo */}
          <Section style={{ padding: "32px 0", textAlign: "center" }}>
            <Img
              src="https://yourapp.com/logo.png"
              width={120}
              height={40}
              alt="YourApp"
            />
          </Section>

          {/* Hero section - need tables for Outlook */}
          <Section style={{ backgroundColor: "#ffffff", borderRadius: "8px" }}>
            <Text style={{ fontSize: "24px", fontWeight: "bold" }}>
              Welcome aboard, {name}
            </Text>
            <Text style={{ color: "#64748b", lineHeight: "1.6" }}>
              You are on the {plan} plan. Here is what to do next...
            </Text>
            {/* ... 150 more lines of inline styles,
                 Outlook conditional comments,
                 mobile responsive hacks ... */}
          </Section>
        </Container>
      </Body>
    </Html>
  );
}

That is one template. You have 12 more: email verification, password reset, trial ending, payment receipt, failed payment, invoice, subscription renewal, cancellation confirmation. Each one needs the same boilerplate and the same Outlook workarounds. It adds up fast.

This is usually the point where someone on the team asks: “Do we really need to build all of these ourselves?”


DNS cutover: running both providers in parallel

You don't need to do a hard cutover. Both SendGrid and Resend can coexist during the transition. The key is DNS record ordering.

DNS records during migration
# Keep existing SendGrid records
TXT  _dmarc.yourapp.com   "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourapp.com"
TXT  yourapp.com           "v=spf1 include:sendgrid.net include:resend.dev ~all"
CNAME s1._domainkey.yourapp.com  s1.domainkey.u12345.wl.sendgrid.net

# Add Resend DKIM records alongside
CNAME resend._domainkey.yourapp.com  resend._domainkey.resend.dev

The SPF record includes both sendgrid.net and resend.dev. This lets you send from either provider while you migrate individual email flows one at a time.

Once all flows are on Resend, remove the include:sendgrid.netfrom your SPF record. Keeping unused SPF includes doesn't hurt deliverability, but it adds unnecessary DNS lookups. SPF has a 10-lookup limit - every include counts.

Migrate flow by flow, not all at once

The safest migration strategy: move one email flow at a time, starting with the lowest-risk template.

PriorityEmail FlowRisk LevelWhy This Order
1Welcome emailLowNon-critical, easy to test, high volume for quick validation
2Email verificationLowSimple template, clear success/failure signal
3Password resetMediumSecurity-critical but low volume, test thoroughly
4Trial ending / lifecycleMediumRevenue impact, but time-delayed so you can monitor
5Payment receipt / invoiceHighCompliance requirements, financial data, migrate last
6Failed payment recoveryHighDirect revenue impact, ensure deliverability is stable first

Recommended migration order: low risk first, revenue-critical last

Each flow you migrate is a chance to verify Resend's deliverability with your domain. By the time you reach the high-risk payment flows, you'll have weeks of data confirming everything works.


What teams actually save

The numbers on a typical SaaS migration with 10-15 email templates tell the story pretty clearly.

ItemDIY from ScratchWith Pre-Built Templates
Template development40-80 hours (3-5 hrs per template x 10-15)4-8 hours (customizing colors, copy, logo)
Cross-client testing15-20 hours (Outlook, Gmail, Apple Mail, Yahoo)0 hours (pre-tested across 90+ clients)
Mobile responsiveness10-15 hours (breakpoints, font scaling, touch targets)0 hours (built in)
Dark mode support8-12 hours (per-client media queries, fallbacks)0 hours (built in)
Total engineering time73-127 hours4-8 hours
At $150/hr senior dev rate$10,950-$19,050$600-$1,200 + template cost

Time comparison: building React Email templates from scratch vs. using a pre-built library

The template cost pays for itself before you finish customizing the second one. But the bigger win is the cross-client testing you skip entirely. Every template has already been verified across Outlook (2016-2024), Gmail, Apple Mail, Yahoo Mail, and Outlook.com. That testing alone would take a week if you did it yourself.


Common gotchas during the switch

1. Suppression list migration

SendGrid maintains a suppression list of addresses that bounced or unsubscribed. Export this list before you cancel your SendGrid account. Sending to suppressed addresses from Resend will damage your new domain reputation.

Export SendGrid suppressions
# Export bounces and unsubscribes from SendGrid API
curl -X GET "https://api.sendgrid.com/v3/suppression/bounces" \
  -H "Authorization: Bearer $SENDGRID_API_KEY" \
  -H "Content-Type: application/json" | jq '.[] | .email' > bounces.txt

curl -X GET "https://api.sendgrid.com/v3/suppression/unsubscribes" \
  -H "Authorization: Bearer $SENDGRID_API_KEY" \
  -H "Content-Type: application/json" | jq '.[] | .email' > unsubscribes.txt

2. Webhook event format differences

SendGrid and Resend use different webhook payload formats. If you process delivery events, bounces, or opens, your webhook handler needs updating.

app/api/email-webhook/route.ts
// SendGrid webhook event
interface SendGridEvent {
  event: "delivered" | "bounce" | "open";
  email: string;
  sg_message_id: string;
  timestamp: number;
}

// Resend webhook event
interface ResendEvent {
  type: "email.delivered" | "email.bounced" | "email.opened";
  data: {
    email_id: string;
    to: string[];
    created_at: string;
  };
}

// Your webhook handler needs to handle the new format
export async function POST(req: Request) {
  const body = await req.json();

  // Resend uses dot-notation event types
  switch (body.type) {
    case "email.delivered":
      await handleDelivered(body.data);
      break;
    case "email.bounced":
      await handleBounce(body.data);
      break;
  }

  return Response.json({ received: true });
}

3. Rate limits are different

SendGrid's free tier allowed 100 emails/day. Resend's free tier allows 3,000 emails/month (100/day). The paid tiers are where the real difference shows: Resend's $20/month plan includes 5,000 emails, while SendGrid's $19.95 Essentials plan includes 100,000 but charges overage fees that add up fast.

For most early-stage SaaS apps sending under 5,000 transactional emails per month, Resend's pricing is simpler and more predictable. SendGrid's volume tiers make more sense above 50,000 emails/month.

After the migration: what you gain

The migration itself is a one-time cost. The day-to-day difference in how you work with email templates is what makes people glad they switched.

WorkflowSendGridResend + React Email
Edit a templateLog into web UI, find template, edit in browser, save, hope it worksEdit .tsx file, see changes in local preview, commit to git
Review template changesNo diff view, no PR review, no audit trailStandard PR review with full diff, visual preview in CI
Test a templateSend test email from dashboard, check your inbox manuallyRun local dev server, hot reload, render tests in CI
Share a template across teamsDuplicate in SendGrid UI, hope both stay in syncImport shared component, single source of truth
Debug rendering issuesView raw HTML in SendGrid, guess which client breaksReact Email CSS checker flags unsupported properties per client

Developer experience comparison after migration


Key takeaway
  • Export your SendGrid suppression lists (bounces + unsubscribes) before cancelling
  • Add Resend DNS records alongside SendGrid - both providers can coexist during migration
  • Replace @sendgrid/mail API calls with the Resend SDK - the surface area is smaller and fully typed
  • Migrate templates flow by flow: welcome first, payment flows last
  • Use production-ready React Email templates instead of rebuilding from scratch - saves 60-120 hours of engineering time
  • Update webhook handlers for Resend's event format (dot-notation types, different payload structure)
  • Remove SendGrid SPF include from DNS once all flows are migrated
  • Monitor bounce rates in Resend dashboard for the first 48 hours after cutover
R

React Emails Pro

Team

Building production-ready email templates with React Email. Writing about transactional email best practices, deliverability, and developer tooling.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates