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: guaranteesservice.tsandwebhooks/handler.tscan 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.tsis 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.tsis internal. It holds the actual Resend client and the logic that talks to the API.actions.tswraps 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.tsdon'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 toactions.ts, that belongs to your own form/component code, not to this module.webhooks/handler.tsis internal signature-verification and event-dispatch logic.webhooks/route.tsis 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 passingscheduledAt. Supports multiple recipients, CC, BCC, reply-to, tags, metadata, and an idempotency key.scheduleEmail— explicit alias for sending with a futurescheduledAt, 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
sendEmailfrom 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.tsto 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 aroundBaseTemplate(or a layout directly) with just the specific content.