@molecule/api-emails-resend
Provider bond · emails · API (Node) · v1.0.2 · Apache-2.0
Resend transactional email provider
npm install @molecule/api-emails-resendnpm · Source on GitHub · Implements @molecule/api-emails
How it works
@molecule/api-emails-resend is a provider bond on the API (Node) side: it implements the emails core interface (@molecule/api-emails) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setTransport } from '@molecule/api-emails'
import { provider } from '@molecule/api-emails-resend'
setTransport(provider)Works with: @molecule/api-bond, @molecule/api-emails, @molecule/api-secrets
Secrets: RESEND_API_KEY, RESEND_FROM (optional)
Reference
Resend email provider for molecule.dev.
Quick Start
import { setTransport } from '@molecule/api-emails'
import { provider } from '@molecule/api-emails-resend'
setTransport(provider)
Type
provider
Installation
npm install @molecule/api-emails-resend @molecule/api-analytics @molecule/api-bond @molecule/api-emails @molecule/api-secrets
API
Interfaces
EmailMessage
Email message options.
interface EmailMessage {
/**
* Sender address.
*/
from: string | EmailAddress
/**
* Recipient(s).
*/
to: string | EmailAddress | (string | EmailAddress)[]
/**
* CC recipient(s).
*/
cc?: string | EmailAddress | (string | EmailAddress)[]
/**
* BCC recipient(s).
*/
bcc?: string | EmailAddress | (string | EmailAddress)[]
/**
* Reply-to address.
*/
replyTo?: string | EmailAddress
/**
* Email subject.
*/
subject: string
/**
* Plain text body.
*/
text?: string
/**
* HTML body.
*/
html?: string
/**
* File attachments.
*/
attachments?: EmailAttachment[]
/**
* i18n key for the subject (for client-side translation).
*/
subjectKey?: string
/**
* i18n key for the plain text body (for client-side translation).
*/
textKey?: string
/**
* i18n key for the HTML body (for client-side translation).
*/
htmlKey?: string
}
EmailSendResult
Result of sending an email.
interface EmailSendResult {
/**
* Whether the email was accepted for delivery.
*/
accepted: string[]
/**
* Addresses that were rejected.
*/
rejected: string[]
/**
* Message ID from the provider.
*/
messageId?: string
/**
* Raw response from the provider.
*/
response?: string
}
EmailTransport
Email transport interface.
All email providers must implement this interface.
interface EmailTransport {
/**
* Sends an email message.
* @returns The send result.
*/
sendMail(message: EmailMessage): Promise<EmailSendResult>
}
ResendAttachment
One entry of a send request's attachments[], exactly as the Resend REST API
expects it: base64 content plus snake_case metadata.
interface ResendAttachment {
/** File content, base64-encoded. */
content: string
/** File name shown to the recipient; Resend derives the MIME type from it when `content_type` is unset. */
filename: string
/** MIME type of the file. */
content_type?: string
/** Content-ID for an inline image referenced as `cid:` in the HTML body. */
content_id?: string
}
ResendClient
The narrow HTTP client this bond uses to reach Resend.
interface ResendClient {
/**
* Sends one email via `POST /emails`.
* @returns The HTTP status and the message id.
*/
send(request: ResendSendRequest): Promise<ResendSendResponse>
}
ResendErrorBody
Resend's JSON error body ({ statusCode, message, name }), with every field
optional because the wire is not trusted.
interface ResendErrorBody {
/** HTTP status Resend reports in the body (may be `null`). */
statusCode?: number | null
/** Human-readable error message. */
message?: string
/** Machine-readable error name, e.g. `validation_error`, `daily_quota_exceeded`. */
name?: string
}
ResendSendRequest
Request body for POST /emails. Field names are the REST API's own
(snake_case, e.g. reply_to) — NOT the camelCase the official SDK accepts.
interface ResendSendRequest {
/** Sender, as a bare address or `Name <address>`. */
from: string
/** Recipients (Resend accepts at most 50). */
to: string[]
/** Subject line. */
subject: string
/** CC recipients. */
cc?: string[]
/** BCC recipients. */
bcc?: string[]
/** Reply-To address. */
reply_to?: string
/** HTML body. */
html?: string
/** Plain-text body (Resend derives one from `html` when omitted). */
text?: string
/** File attachments. */
attachments?: ResendAttachment[]
}
ResendSendResponse
What ResendClient.send resolves with: the HTTP status of the accepted
request and the message id Resend returned in the response body.
interface ResendSendResponse {
/** HTTP status code of the response (2xx). */
status: number
/** The `id` from the response body, when present. */
id?: string
}
Classes
ResendApiError
A non-2xx response from the Resend API.
status is the HTTP status, code is Resend's machine-readable error name
(e.g. validation_error, rate_limit_exceeded, daily_quota_exceeded) when
the body carried one, and body is the parsed error body. Deliberately NOT
tagged with statusCode / errorKey: Resend's status describes OUR request
to Resend (a 401 is a bad key, a 403 an unverified domain), not the caller's
request to the app, so the API middleware must fall through to its generic
500 rather than echo it.
Functions
getClient()
Returns the Resend HTTP client. Nothing is configured up front: the API key
and base URL are read from the environment on every send().
function getClient(): ResendClient
Returns: The Resend client.
sendMail(message)
Sends an email through the Resend API.
function sendMail(message: EmailMessage): Promise<EmailSendResult>
message— The email message (to, from, subject, text/html, attachments).
Returns: Send result with accepted addresses, message ID, and status code.
Constants
emailsResendSecretDefinitions
Secret definitions required by the Resend email bond.
const emailsResendSecretDefinitions: SecretDefinition[]
provider
The Resend email provider implementing the standard interface.
const provider: EmailTransport
RESEND_DEFAULT_BASE_URL
Resend's REST API base URL. Override with RESEND_BASE_URL.
const RESEND_DEFAULT_BASE_URL: 'https://api.resend.com'
Core Interface
Implements @molecule/api-emails interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setTransport } from '@molecule/api-emails'
import { provider } from '@molecule/api-emails-resend'
export function setupEmailsResend(): void {
setTransport(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-emails^1.0.1@molecule/api-secrets^1.0.1
Environment Variables
RESEND_API_KEY(required) — Resend API key- Setup: Resend → API Keys → Create API Key (Sending access). Verify your sending domain under Domains first.
- Get it here: https://resend.com/api-keys
- Example:
re_...
RESEND_FROM(optional) — Default sender- Setup: Optional default From address on a verified domain, e.g. "Acme hello@acme.com".
- Get it here: https://resend.com/domains
- Example:
Acme <hello@example.com>
Runtime Dependencies
-
@molecule/api-analytics -
@molecule/api-bond -
@molecule/api-emails -
@molecule/api-secrets -
Zero dependencies — talks to Resend's REST API with the runtime's global
fetch(POST https://api.resend.com/emails), so every app that installs this bond stays dependency-free; the officialresendSDK can be swapped in later without changing the bond's interface. -
fromMUST be on a domain you VERIFIED in Resend (Resend → Domains, DNS records added and checked), or the API rejects the send with a 403validation_error("domain is not verified"). Before any domain is verified the only usable sender isonboarding@resend.dev— testing only, and it can deliver ONLY to the account owner's own address. Read the sender from config, never hardcode a placeholder: setRESEND_FROM(e.g.Acme <no-reply@your-verified-domain.com>) and it is used whenever a message'sfromis empty; if both are missing,sendMail()throws a tagged config-missing error namingRESEND_FROM. -
Configuration is lazy and env-driven:
RESEND_API_KEY(and the optionalRESEND_BASE_URLoverride for brokers / compatible endpoints) are read on EACH send — never at import time — so a key resolved intoprocess.envafter import (late secrets resolution via a secrets bond) is honored. If the key is absent at send time,sendMail()throws a tagged config-missing error (clean 503 /config.notConfigured) namingRESEND_API_KEY— never an opaque Resend 401. -
There is NO sandbox / test-mode flag in the Resend API (nothing like SendGrid's
sandboxMode). Every accepted request is a real send that counts against the account's quota — including sends to the simulation recipientsdelivered@resend.dev,bounced@resend.devandcomplained@resend.dev, which are the supported way to exercise the delivery / bounce / spam paths without emailing a real inbox. -
Attachments are sent base64-encoded inline; Resend caps the whole email at 40 MB after encoding (larger sends are rejected). Buffer / string content only — a stream attachment throws. A
cidbecomes Resend'scontent_idfor inline images (<img src="cid:...">). -
API errors surface as
ResendApiErrorcarryingstatus(HTTP) andcode(Resend's error name —validation_error,rate_limit_exceeded,daily_quota_exceeded, …). They are deliberately NOT tagged withstatusCode/errorKey, so the API middleware returns its generic 500 instead of echoing Resend's status to the caller. On successacceptedechoes everytorecipient (Resend returns no per-recipient verdict) andmessageIdis theidfrom the response body.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks). The
sandbox CAPTURES outbound email instead of sending — read each message with
the read_activity tool (filter type 'email'); the verification/reset link
is in its payload. Never mock the send or modify production code to expose
it. Adapt each item to this app's actual screens/flows, and check every box
off one by one. A box you can't check is an integration bug to fix — not a
skip:
- Each email-triggering flow (signup verification, password-reset request, invites/notifications the app defines) confirms the send in the UI ("check your inbox") and a message actually reaches the transport.
- The password-reset round-trip completes: request a reset → open the captured message → follow its single-use link → set a new password → log in with it (and the old password no longer works).
- The message body contains a LINK, never the raw token/secret, and renders
with the app's real name/content (no
undefinedplaceholders). - Requesting a reset for an unknown email shows the same neutral UI response as a known one (no account-existence oracle).
- Account emails go only to the account's own address — no UI or endpoint lets an unauthenticated caller send to an arbitrary address.