Resend
Send transactional email, such as contact-form messages, through Resend.
@decocms/apps-resend sends email through Resend's HTTP API. The typical use is a contact form: editors set the API key, sender, recipients and subject in Studio, and your site sends a message with one call. It has a single action, sendEmail.
bun add @decocms/apps-resendConfiguring
Editors configure the app in a deco-resend block:
| Field | What it does |
|---|---|
apiKey | Your Resend API key: plain text or an encrypted secret. Falls back to the RESEND_API_KEY environment variable. |
emailFrom.name | The sender's display name. Defaults to Contact. |
emailFrom.domain | The sender's full address, such as hello@store.example.com, despite the field's name. It's placed inside the angle brackets of Name <address>. Defaults to onboarding@resend.dev, Resend's test sender. The address must be on a domain you've verified in Resend. |
emailTo | Default recipients, as a list of addresses. |
subject | Default subject. |
configure returns null when no API key resolves, and the app isn't installed: sendEmail then throws Resend not configured. Install it with the registry entry (see Apps):
import { autoconfigApps, type AppRegistry } from "@decocms/blocks-admin/apps";
import { loadBlocks } from "@decocms/blocks/cms";
import { RESEND_REGISTRY_ENTRY } from "@decocms/apps-resend/registry";
import * as resendMod from "@decocms/apps-resend/mod";
const APP_REGISTRY: AppRegistry = [{ ...RESEND_REGISTRY_ENTRY, module: async () => resendMod }];
await autoconfigApps(loadBlocks(), APP_REGISTRY);To configure it from code instead, call configureResend once at module scope. You then resolve the key yourself; this path doesn't decrypt secrets:
import { configureResend } from "@decocms/apps-resend/client";
configureResend({
apiKey: process.env.RESEND_API_KEY ?? "",
emailFrom: "My Store <hello@store.example.com>",
emailTo: ["team@store.example.com"],
subject: "Contact form submission",
});ResendConfig.emailFrom here is the complete Name <address> string.
Sending email
sendEmail(payload) merges what you pass over the configured defaults and posts the message to Resend:
| Field | Default |
|---|---|
from | The configured sender, else Contact <onboarding@resend.dev> |
to | The configured emailTo, else an empty list (which Resend rejects) |
subject | The configured subject, else No subject |
html, text, cc, bcc, reply_to, headers | Sent only when you set them |
It never throws for an error Resend reports. It always resolves to { data, error }:
import { sendEmail } from "@decocms/apps-resend";
export interface Props {
name: string;
email: string;
message: string;
}
export default async function contact(props: Props) {
const { data, error } = await sendEmail({
subject: `Message from ${props.name}`,
reply_to: props.email,
text: props.message,
});
if (error) return { ok: false, reason: error.name };
return { ok: true, id: data?.id };
}On success data is { id } and error is null. On failure data is null and error is Resend's { message, name }, where name is one of Resend's error codes, such as validation_error, invalid_from_address, rate_limit_exceeded or missing_api_key (the full list is RESEND_ERROR_CODES_BY_KEY in @decocms/apps-resend/types). Resend limits how fast you can send; see its rate limits. Network failures and timeouts, after 10 seconds, still throw. Calling sendEmail before the app is configured throws Resend not configured.
To build HTML emails as React components, render them on the server with React Email and pass the result as html.
Calling it from a form
Call sendEmail from server code you own, not from the browser. For a form, write a small action like contact above: it fixes the recipients and subject on the server, accepts only the fields the form needs, and is the place to add validation and spam protection. Put it in src/actions/ and the generate command makes it callable as site/actions/contact (see Loaders and actions).
Related
- Apps: installing apps and secrets.
- Loaders and actions: site actions and invoke.