PingMyUsers

How to send email from Next.js with React Email (2026)

Editorial team · updated · facts checked

To send email from Next.js with React Email, install the react-email package, write each email as a React component, call await render() on the server to get HTML and toPlainText() for the text part, then pass both strings to your provider's SDK from a Server Action or Route Handler that reads the API key from a server-only environment variable.

React Email is an open-source MIT library maintained by Resend, and its docs call Resend "our recommendation" for sending. What render() returns is an ordinary HTML string, so any provider can send it: this tutorial keeps the provider in one file and shows that file for five of them. We read every page cited here on 23 September 2026 (react-email 6.9.5, Next.js 16.3.6) but did not run the code or send email, so every snippet is illustrative.

Before you start

  • A Next.js project on the App Router.
  • Node.js 20 or later: react-email 6.9.5 and resend 6.28.1 both declare node >=20 on npm.
  • An email provider account and a domain whose DNS you control (see "Authenticate your sending domain" below).

Which React Email packages to install in 2026

React Email 6.0.0, released on 16 April 2026, moved every component and the render utilities into one package, react-email, and renamed the preview server package (update guide). npm now marks @react-email/components as deprecated, yet much existing code still pulls it in: 4.1 million downloads in the week of 15–21 September 2026, more than react-email itself (3.0 million). Tutorials written before April 2026 use the old names.

If older code usesUse now
@react-email/componentsreact-email
render or toPlainText from @react-email/renderthe same functions from react-email, which re-exports that package
@react-email/preview-server@react-email/ui, as a dev dependency
renderAsync()await render() (the change came in React Email 5.0)
React Email documentation page Updating React Email, section Update from React Email 5.0 to 6.0, saying components and render utilities are now exported from react-email and the preview server moved to @react-email/ui
React Email's update guide, section 5.0 to 6.0: the package changes this tutorial's imports follow. Captured by the editorial team on 23 September 2026.

Step 1. Install React Email in your Next.js app

npx create-email@latest builds a separate react-email-starter folder, so for an existing app follow React Email's manual setup. Your project already has react and react-dom; react-email accepts React 18 or 19.

Illustrative code, written from the official documentation and not run by the editorial team.

npm install react-email -E
npm install @react-email/ui -D -E
npm install server-only

Then add a preview script to package.json:

{
  "scripts": {
    "email:dev": "email dev --dir emails --port 3001"
  }
}

The email dev preview and next dev both default to port 3000 (React Email CLI, Next.js CLI), so the preview gets 3001. If your code lives in src/, use --dir src/emails.

Step 2. Write the email as a React component

Illustrative code, written from the official documentation and not run by the editorial team.

// emails/welcome.tsx (illustrative, not run)
import * as React from "react";
import { Body, Button, Container, Head, Heading, Html, Preview, Text } from "react-email";

type WelcomeEmailProps = { name: string; dashboardUrl: string };

export default function WelcomeEmail({ name, dashboardUrl }: WelcomeEmailProps) {
  return (
    <Html lang="en">
      <Head />
      <Preview>Your Acme account is ready</Preview>
      <Body style={{ backgroundColor: "#f6f7f9", fontFamily: "Arial, sans-serif" }}>
        <Container style={{ backgroundColor: "#ffffff", padding: "24px", maxWidth: "560px" }}>
          <Heading style={{ fontSize: "22px" }}>Welcome, {name}</Heading>
          <Text>Your account is ready. Open the dashboard to finish setup.</Text>
          <Button
            href={dashboardUrl}
            style={{ backgroundColor: "#111827", color: "#ffffff", padding: "12px 20px", borderRadius: "6px" }}
          >
            Open dashboard
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

// Sample data for the preview server only
WelcomeEmail.PreviewProps = { name: "Ada", dashboardUrl: "https://example.com/dashboard" } satisfies WelcomeEmailProps;

The components ship unstyled, so styles go in style props or inside the Tailwind component, which uses Tailwind 4 since React Email 5. <Preview> sets the line inbox apps show after the subject. The preview server lists a .tsx file only if it has an export default; put shared pieces in a folder whose name starts with _, such as emails/_components, to keep them off the list.

Step 3. Preview the template

Run npm run email:dev, open http://localhost:3001, and edit the file; the preview rebuilds on save with the PreviewProps data.

Images in emails/static are served by the preview server only, the CLI docs warn, so a sent email needs absolute image URLs on a public host; switch the base URL on NODE_ENV. The toolbar's Resend tab is optional: it uploads templates to Resend's hosted Templates and needs a full-access Resend key.

Step 4. Render HTML and plain text on the server

Illustrative code, written from the official documentation and not run by the editorial team.

// lib/email/welcome.tsx (illustrative, not run)
import "server-only";
import { render, toPlainText } from "react-email";
import WelcomeEmail from "@/emails/welcome";

export async function renderWelcomeEmail(props: { name: string; dashboardUrl: string }) {
  const html = await render(<WelcomeEmail {...props} />);
  const text = toPlainText(html);
  return { html, text };
}

render() returns a Promise of the HTML string, per the type in @react-email/render 2.1.0, so it needs await. toPlainText() builds the text part; add data-skip-in-text="true" to an element to leave it out of the text (render docs). Resend builds a text part from HTML if you omit it; the other SDKs in Step 6 take text as a separate field. The server-only import turns any Client Component import of this file into a build error (Next.js docs).

Step 5. Send from a Server Action or a Route Handler

Use a Server Action when your own UI triggers the email, such as a form. Use a Route Handler when something outside the UI calls your app: a job runner, another service, a webhook.

Keep the API key server-side

Illustrative code, written from the official documentation and not run by the editorial team.

# .env.local (create-next-app's default .gitignore excludes .env files)
RESEND_API_KEY=re_xxxxxxxx
EMAIL_FROM="Acme <hello@notify.example.com>"

No NEXT_PUBLIC_ prefix: Next.js inlines those values into browser JavaScript at build time, and replaces unprefixed variables in client code with an empty string (environment variables guide). On Vercel, add the same names under the project's environment variables; Vercel applies changes to new deployments only, so redeploy.

The send function, the only provider-specific file

The example uses Resend for two reasons: it was the only provider AI search engines named when we researched this question, and React Email's own docs have no Next.js page while Resend's docs have one. Nothing outside this file depends on that choice; Step 6 has the same function for four other providers.

Illustrative code, written from the official documentation and not run by the editorial team.

// lib/email/send.ts (illustrative, not run). npm install resend
import "server-only";
import { Resend } from "resend";

export type OutgoingEmail = { to: string; subject: string; html: string; text: string; idempotencyKey?: string };

export async function sendEmail(email: OutgoingEmail) {
  const resend = new Resend(process.env.RESEND_API_KEY); // created per call, so a missing key fails here, not at import
  const { data, error } = await resend.emails.send(
    { from: process.env.EMAIL_FROM!, to: [email.to], subject: email.subject, html: email.html, text: email.text },
    { idempotencyKey: email.idempotencyKey }, // second argument, per the SDK type
  );
  if (error) throw new Error(`Email not sent: ${error.name}`); // the SDK returns errors instead of throwing
  return data?.id;
}

Resend keeps an idempotency key for 24 hours, so a retry with the same key does not send twice (idempotency docs). The SDK can also render the component itself through a react field, but Resend's Next.js guide passes WelcomeEmail({ name }) as a function call while React Email's Resend page passes JSX. Rendering yourself avoids the question.

From a Server Action

Illustrative code, written from the official documentation and not run by the editorial team.

// app/actions/welcome.ts (illustrative, not run)
"use server";
import { getCurrentUser } from "@/lib/auth"; // your auth library
import { renderWelcomeEmail } from "@/lib/email/welcome";
import { sendEmail } from "@/lib/email/send";

// Call it from a form or button: <form action={sendWelcomeEmail}><button>Send welcome email</button></form>
export async function sendWelcomeEmail() {
  const user = await getCurrentUser();
  if (!user) throw new Error("Unauthorized");

  const { html, text } = await renderWelcomeEmail({ name: user.name, dashboardUrl: "https://example.com/dashboard" });
  await sendEmail({ to: user.email, subject: "Welcome to Acme", html, text, idempotencyKey: `welcome/${user.id}` });
}

Next.js warns that Server Functions are reachable by direct POST requests, not only through your UI, so check authentication inside every one (mutating data). Take the recipient from the session or your database, never from form input; otherwise any signed-in user can send mail from your domain to any address.

From a Route Handler

Illustrative code, written from the official documentation and not run by the editorial team.

// app/api/emails/welcome/route.ts (illustrative, not run)
import { timingSafeEqual } from "node:crypto";
import { db } from "@/lib/db"; // your data layer
import { renderWelcomeEmail } from "@/lib/email/welcome";
import { sendEmail } from "@/lib/email/send";

function authorized(req: Request) {
  const secret = process.env.INTERNAL_API_SECRET;
  if (!secret) return false; // unset secret must never match "Bearer undefined"
  const got = Buffer.from(req.headers.get("authorization") ?? "");
  const want = Buffer.from(`Bearer ${secret}`);
  return got.length === want.length && timingSafeEqual(got, want);
}

export async function POST(req: Request) {
  if (!authorized(req)) return new Response(null, { status: 401 });
  const { userId } = await req.json();
  const user = await db.users.find(userId);
  if (!user) return Response.json({ error: "not found" }, { status: 404 });

  const { html, text } = await renderWelcomeEmail({ name: user.name, dashboardUrl: "https://example.com/dashboard" });
  const id = await sendEmail({ to: user.email, subject: "Welcome to Acme", html, text, idempotencyKey: `welcome/${user.id}` });
  return Response.json({ id });
}

POST Route Handlers are not cached and run on the Node.js runtime by default; the Next.js 16.3.6 docs mark export const runtime = 'edge' as deprecated, so leave it out. For payment or auth webhooks that sign their requests, verify the signature on the raw body instead of a shared secret, as in the receipt scenario of our transactional email setup guide. Most email providers sign their bounce and complaint webhooks as well, though Postmark relies on Basic auth instead; each provider card shows the scheme.

Step 6. Swap the email provider

Rewrite sendEmail with another SDK; the template and callers stay as they are. React Email's docs have sending pages for Nodemailer and more than ten providers (integrations overview); these are the five in our catalog.

Providernpm package (latest, 23 Sep 2026)Call inside sendEmailFree option (our data, checked 23 Sep 2026)
Amazon SES@aws-sdk/client-sesv2 3.1138.0new SESv2Client({ region }).send(new SendEmailCommand({ FromEmailAddress, Destination: { ToAddresses: [to] }, Content: { Simple: { Subject: { Data: subject }, Body: { Html: { Data: html }, Text: { Data: text } } } } }))No always-free quota; up to $200 in AWS credits for new customers (card required); sandbox sends only to verified addresses
Mailgunmailgun.js 14.0.1new Mailgun(FormData).client({ username: "api", key }).messages.create(domain, { from, to: [to], subject, html, text })100 emails a day; sandbox domain sends to up to 5 authorized recipients
Postmarkpostmark 5.1.0new ServerClient(token).sendEmail({ From, To, Subject, HtmlBody, TextBody })100 emails a month, no expiry; own domains only until the account is approved
Resendresend 6.28.1Step 53,000 emails a month, capped at 100 a day
SendGrid@sendgrid/mail 8.1.6sgMail.setApiKey(key); await sgMail.send({ to, from, subject, html, text })60-day trial at 100 emails a day; no free-forever plan

Calls follow each vendor's SDK docs under Sources; free options come from our provider data files. Three details to watch: React Email's own AWS SES page uses the older @aws-sdk/client-ses (SES API v1); the row above is API v2. Mailgun accounts on EU infrastructure also pass url: "https://api.eu.mailgun.net". Postmark sends through the default transactional stream, outbound, unless you set MessageStream.

The other four document no idempotency key for these send calls, so record a "sent" marker keyed on the event before calling, as our transactional email guide explains in its email API vs SMTP section. Prices at your volume, AI readiness and reputation are compared in our transactional email API rankings.

Authenticate your sending domain

Every provider here sends to other people only from a verified domain or sender. Before that, Resend returns a 403 validation_error for any recipient other than your own address, SendGrid returns 403 Forbidden without a verified sender identity, and Postmark returns 422 when the From address has no confirmed sender signature. Use a subdomain such as notify.example.com and publish the SPF, DKIM and DMARC records your provider gives you; the SPF, DKIM and DMARC walkthrough covers the records and the Gmail, Yahoo and Outlook rules.

Common errors

SymptomCauseFix
npm warns that @react-email/components is deprecatedCode written for React Email 5 or earlierUninstall it and import everything from react-email
Old tutorial code calls renderAsyncReplaced by render in React Email 5.0await render(...)
TypeScript: Promise<string> is not assignable to stringrender() called without awaitAwait it before passing html
Missing API key. Pass it to the constructornew Resend() found no RESEND_API_KEY in that environment (message from the resend 6.28.1 source)Set the variable where the code runs; on Vercel, redeploy after adding it
Build error that a module can only be used on the serverA Client Component imports lib/email/*Call a Server Action from the component instead
Resend 403: "You can only send testing emails to your own email address"Sending from the test domainVerify a domain and use it in from
Resend 429Default limit of 10 requests per second per team, or the free plan's daily quotaQueue sends, or move to a paid plan
Images show in the preview but not in the inboxemails/static is served by the preview server onlyAbsolute URLs on a public host

Resend's error codes and limits come from its errors reference and quota page.

If an AI assistant writes the code

Assistants working from older tutorials can bring back the deprecated imports, so name the packages. React Email's llms.txt, a docs map for language models, links an official React Email agent skill, and Resend's resend-skills plugin includes one too. Our MCP servers and agent skills dataset lists what each email provider publishes for agents. A prompt that pins the choices in this tutorial, which we have not run:

Add a welcome email to this Next.js App Router app with React Email 6.
- Install react-email (components and render come from it) and @react-email/ui as a dev dependency.
  Do not use @react-email/components or renderAsync.
- Template in emails/welcome.tsx with PreviewProps; "email:dev" script on port 3001.
- lib/email/welcome.tsx renders HTML with await render() and text with toPlainText(); import "server-only".
- lib/email/send.ts is the only file that knows the provider ([PROVIDER], docs: [URL]).
  Read the key from process.env.[PROVIDER]_API_KEY; never NEXT_PUBLIC_, never hardcoded or logged.
- Send from a Server Action that checks the signed-in user and takes the address from the session.
- Then list every file that reads the key so I can confirm none is a Client Component.

About this guide

The PingMyUsers editorial team wrote this tutorial for developers adding email to a Next.js app; Sensaria AG in Switzerland operates the directory. We did not create provider accounts, run the code or send email. No provider paid for placement or reviewed this page. Resend appears in the main example for the reasons given in Step 5; that is not a recommendation. Report errors to contact@sensaria.ch with a source.

Methodology

On 23 September 2026 we read React Email's docs and changelog, the Next.js 16.3.6 docs and each provider's SDK page listed under Sources. Package versions, the deprecation flag and weekly download counts come from the npm registry that day; function signatures come from the published type definitions. Free options come from our provider data files, where each field has a source, an evidence status and a check date. Section order follows the searches GPT and Gemini ran for ten phrasings of this question; GPT answered 8 of 50 attempts, the rest hit API rate limits. Editorial rules are on the methodology page.

Last updated

23 September 2026: first version, all facts checked that day. Next check: at publication, then March 2027 or at the next React Email or Next.js major release.

Frequently asked questions

What is React Email?

An open-source MIT library, maintained by Resend, that turns React components into HTML email. Since version 6 (16 April 2026), components and render ship in one package, react-email.

Do I need Resend to use React Email?

No. render() returns an HTML string that any provider's API or SMTP relay accepts. React Email's docs include sending guides for SendGrid, Postmark, AWS SES, Mailgun, Nodemailer and others.

Is @react-email/components deprecated?

Yes. React Email 6 moved its components into react-email, and npm marks @react-email/components as deprecated. Replace the imports; the component names are the same.

Should I send email from a Server Action or a Route Handler?

A Server Action when your own UI triggers the email; a Route Handler when an outside caller, such as a job or a webhook, does. Both need an auth check.

Where do I put the email API key in Next.js?

In .env.local without the NEXT_PUBLIC_ prefix, and in your host's environment variables for production. Read it only in files that import server-only.

How do I create a plain-text version of a React Email template?

Pass the rendered HTML to toPlainText(), or call render() with { plainText: true }. Mark parts to leave out with data-skip-in-text="true".

Can I send email from the Edge runtime in Next.js?

The Next.js 16.3.6 docs mark the Edge runtime as deprecated for routes. Keep email code on the default Node.js runtime and leave out export const runtime = 'edge'.

Sources

  1. React Email — Introduction — checked 23 September 2026
  2. React Email — Manual setup — checked 23 September 2026
  3. React Email — Updating React Email (5.0 to 6.0) — checked 23 September 2026
  4. React Email — Changelog (React Email 6.0.0, 16 April 2026) — checked 23 September 2026
  5. React Email — Render utility — checked 23 September 2026
  6. React Email — CLI (email dev, PreviewProps, static files) — checked 23 September 2026
  7. React Email — Integrations overview — checked 23 September 2026
  8. React Email — Send email using Resend — checked 23 September 2026
  9. React Email — Send email using AWS SES — checked 23 September 2026
  10. React Email — llms.txt (links the React Email agent skill) — checked 23 September 2026
  11. npm — react-email 6.9.5 — checked 23 September 2026
  12. npm — @react-email/components (marked deprecated) — checked 23 September 2026
  13. npm downloads API — react-email, week of 15–21 September 2026 — checked 23 September 2026
  14. npm downloads API — @react-email/components, week of 15–21 September 2026 — checked 23 September 2026
  15. Next.js — Route Handlers (docs for 16.3.6) — checked 23 September 2026
  16. Next.js — Mutating Data (Server Functions and Server Actions) — checked 23 September 2026
  17. Next.js — Environment variables — checked 23 September 2026
  18. Next.js — Server and Client Components (server-only) — checked 23 September 2026
  19. Next.js — runtime segment config (Edge deprecated) — checked 23 September 2026
  20. Next.js — CLI (default port 3000) — checked 23 September 2026
  21. Vercel — Environment variables — checked 23 September 2026
  22. Resend — Send emails with Next.js — checked 23 September 2026
  23. Resend — Send email API reference — checked 23 September 2026
  24. Resend — Idempotency keys — checked 23 September 2026
  25. Resend — Errors — checked 23 September 2026
  26. Resend — Account quotas and limits — checked 23 September 2026
  27. npm — resend 6.28.1 (Node.js SDK) — checked 23 September 2026
  28. Postmark — Sending email with API — checked 23 September 2026
  29. npm — postmark 5.1.0 — checked 23 September 2026
  30. Twilio SendGrid — Node.js quickstart — checked 23 September 2026
  31. GitHub — @sendgrid/mail README — checked 23 September 2026
  32. AWS SDK for JavaScript v3 — SESv2 SendEmailCommand — checked 23 September 2026
  33. GitHub — mailgun.js README — checked 23 September 2026
  34. Resend — pricing — checked 23 September 2026
  35. Postmark — pricing — checked 23 September 2026
  36. Twilio SendGrid — Email API pricing — checked 23 September 2026
  37. Amazon SES — pricing — checked 23 September 2026
  38. Mailgun — pricing — checked 23 September 2026