Logo FormaUI

FormaUI

All providers

Resend Email

Server-side infrastructure to send transactional and marketing emails with Resend in a Next.js App Router project — client, validation, reusable React Email templates/layouts, and webhook handling. Logic only, no form or UI components included.

Dependencies

  • resend: official Resend SDK — the client used to send, schedule, cancel, and look up emails.
  • @react-email/components: official React Email components used to build the included templates and layouts (Html, Body, Button, etc.).
  • svix: verifies the signature of incoming Resend webhooks — Resend signs its webhooks with Svix.
  • server-only: guarantees service.ts and webhooks/handler.ts can never end up in a client bundle.
  • react-email (dev dependency): CLI used to preview templates locally.

Folder Structure

email/resend/
├── actions.ts               // Public API — "use server" — sendEmail, scheduleEmail, cancelScheduledEmail, rescheduleEmail
├── service.ts                // Internal — Resend client and raw send/schedule/cancel/lookup logic
├── types.ts                   // Shared types (send options, results, errors, webhook events)
├── constants.ts                // Technical config — env var keys, recipient/attachment limits, retry settings
├── brand.ts                     // Content/design config — company name, logo, support email, colors
├── validation.ts                 // Runtime validation of send options + normalization of Resend errors
├── webhooks/
│   ├── handler.ts                 // Internal — Svix signature verification and event dispatch
│   └── route.ts                    // Exact, ready-to-copy content for app/api/webhooks/resend/route.ts
├── templates/
│   ├── base.tsx                     // Generic template (heading + text + optional CTA button)
│   ├── welcome.tsx                   // Welcome email
│   ├── invitation.tsx                 // Invitation email (invite to a team/workspace)
│   └── notification.tsx                // Generic notification email
├── layouts/
│   ├── default.tsx                      // Shared header/footer, aware of transactional vs. marketing variant
│   ├── transactional.tsx                 // Thin wrapper — minimal footer, no unsubscribe link
│   └── marketing.tsx                      // Thin wrapper — footer includes unsubscribe link
├── env.example                             // Required environment variables
└── setup.md                                 // Usage examples

File Roles

  • actions.ts is the public entry point. Every export is a Server Action ("use server"), so it's the only file most consumers ever import from — directly from Server Components, your own Server Actions, or forms.
  • service.ts is internal. It holds the actual Resend client and the logic that talks to the API. actions.ts wraps the core parts of it; two less common operations (resendEmail, getEmailStatus) are exposed only here, without a Server Action wrapper, since they're used less often.
  • client.ts / hooks.ts don't exist in this module. Sending email has no client-side state to manage — there's no interactive widget or hook a Client Component would need, so there's nothing to expose on that side. If your app needs a "sending…" state around a call to actions.ts, that belongs to your own form/component code, not to this module.
  • webhooks/handler.ts is internal signature-verification and event-dispatch logic. webhooks/route.ts is the one file in this module meant to be copied as-is into your app (app/api/webhooks/resend/route.ts) — it's a Route Handler, since a third-party webhook has to be an HTTP endpoint the provider can call, not a Server Action.

What actions.ts Covers

  • sendEmail — send a text, HTML, or React Email message immediately, or schedule it by passing scheduledAt. Supports multiple recipients, CC, BCC, reply-to, tags, metadata, and an idempotency key.
  • scheduleEmail — explicit alias for sending with a future scheduledAt, so the intent reads clearly at the call site.
  • cancelScheduledEmail — cancel a scheduled email before it goes out.
  • rescheduleEmail — move a scheduled email to a different send time.

resendEmail (re-sending a previous email) and getEmailStatus (looking up delivery status by id) are advanced/less common utilities and live only in service.ts.

Usage Examples

Everything you need for standard usage is imported from actions.ts.

Send a plain text email:

import { sendEmail } from "@/email/resend/actions";

await sendEmail({
  from: "Acme <no-reply@your-domain.com>",
  to: "user@example.com",
  subject: "Hello",
  text: "This is a plain text email.",
});

Send HTML instead — only one of text, html, or react can be set per call:

await sendEmail({
  from: "Acme <no-reply@your-domain.com>",
  to: "user@example.com",
  subject: "Hello",
  html: "<p>This is an <strong>HTML</strong> email.</p>",
});

Send one of the included React Email templates by passing it as react:

import { sendEmail } from "@/email/resend/actions";
import { WelcomeEmail } from "@/email/resend/templates/welcome";

await sendEmail({
  from: "Acme <no-reply@your-domain.com>",
  to: "user@example.com",
  subject: "Welcome!",
  react: WelcomeEmail({ recipientName: "John", ctaUrl: "https://yourapp.com/onboarding" }),
});

Build the react/html content on the server if sendEmail is called as a Server Action from a Client Component — a ReactElement argument doesn't survive serialization across that boundary:

"use server";

async function sendWelcome(userEmail: string, userName: string) {
  return sendEmail({
    from: "Acme <no-reply@your-domain.com>",
    to: userEmail,
    subject: "Welcome!",
    react: WelcomeEmail({ recipientName: userName }),
  });
}

Cover multiple recipients, CC, BCC, tags, custom metadata, and an idempotency key to avoid duplicate sends on retry:

await sendEmail({
  from: "Acme <no-reply@your-domain.com>",
  to: ["a@example.com", "b@example.com"],
  cc: "manager@example.com",
  bcc: "audit@example.com",
  replyTo: "support@your-domain.com",
  subject: "Order update",
  html: "<p>Your order was updated.</p>",
  tags: [{ name: "category", value: "order_update" }],
  metadata: { orderId: "1234" },
  idempotencyKey: "order-1234-status-update",
});

Schedule an email for later, then cancel or reschedule it using the id returned by the original call:

import { scheduleEmail, cancelScheduledEmail, rescheduleEmail } from "@/email/resend/actions";

await scheduleEmail(
  {
    from: "Acme <no-reply@your-domain.com>",
    to: "user@example.com",
    subject: "Reminder",
    text: "Don't forget your meeting tomorrow.",
  },
  "2026-08-01T09:00:00Z",
);

await cancelScheduledEmail("email_id_here");
await rescheduleEmail({ id: "email_id_here", scheduledAt: "2026-08-02T09:00:00Z" });

Every function in actions.ts returns a result object instead of throwing for expected errors (validation, rate limits, Resend API errors):

const result = await sendEmail({
  from: "Acme <no-reply@your-domain.com>",
  to: "user@example.com",
  subject: "Hello",
  text: "Hi there.",
});

if (!result.success) {
  console.error(result.error.type, result.error.message);
}

Use sendEmailOrThrow from service.ts instead if you'd rather it throw:

import { sendEmailOrThrow } from "@/email/resend/service";

try {
  await sendEmailOrThrow({
    from: "Acme <no-reply@your-domain.com>",
    to: "user@example.com",
    subject: "Hello",
    text: "Hi there.",
  });
} catch (err) {
  // err.message is already normalized
}

resendEmail and getEmailStatus are imported straight from service.ts:

import { resendEmail, getEmailStatus } from "@/email/resend/service";

await resendEmail({
  originalEmailId: "original_email_id",
  from: "Acme <no-reply@your-domain.com>",
  to: "user@example.com",
  subject: "Re: Order update",
  html: "<p>Resending this in case you missed it.</p>",
});

const status = await getEmailStatus("email_id_here");
if (status.success) console.log(status.data.last_event); // "delivered", "bounced", etc.

Handle webhook events with an explicit if instead of the dispatch object already wired in webhooks/route.ts:

import { verifyWebhookSignatureOrThrow, isBounced } from "@/email/resend/webhooks/handler";

const event = verifyWebhookSignatureOrThrow(payload, headers);

if (isBounced(event)) {
  // event.data.email_id, event.data.to, etc.
}

What you can build with this module

This module is the sending layer, not a full product feature — it's meant to be composed into whatever you're building on top of it:

  • Transactional flows: welcome emails on signup, invitations to a team/workspace, order/payment notifications. The auth-specific logic (verifying a user, resetting a password) belongs to the Auth module, which imports sendEmail from here for the actual delivery.
  • Marketing/broadcast-style emails: use layouts/marketing.tsx (adds the unsubscribe footer) as the base for newsletters or announcements.
  • Delivery observability: wire up webhooks/route.ts to track delivered/bounced/complained events and persist them to your own database — this module intentionally doesn't store send history itself, since Resend has no endpoint to list it.
  • Custom templates: any new email type follows the same pattern as welcome.tsx — a thin wrapper around BaseTemplate (or a layout directly) with just the specific content.