@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.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
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
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
}
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
}
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
}
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>
}
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>
}
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.
class ResendApiError extends Error {
/** HTTP status of the Resend response. */
readonly status: number
/** Resend's error name from the body, when present. */
readonly code: string | undefined
/** The parsed error body, when the response was JSON. */
readonly body: ResendErrorBody | undefined
/**
* Builds the error from a non-2xx Resend response.
* @param status - HTTP status of the response.
* @param body - The parsed error body, if the response was JSON.
* @param rawBody - The raw response text (used in the message when the body was not JSON).
*/
constructor(status: number, body: ResendErrorBody | undefined, rawBody: string) {
const detail = body?.message ?? (rawBody.trim() || 'no response body')
super(`Resend API error ${status}${body?.name ? ` (${body.name})` : ''}: ${detail}`)
this.name = 'ResendApiError'
this.status = status
this.code = body?.name
this.body = body
}
}
Functions
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.
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.
Constants
provider
The Resend email provider implementing the standard interface.
const provider: EmailTransport
emailsResendSecretDefinitions
Secret definitions required by the Resend email bond.
const emailsResendSecretDefinitions: SecretDefinition[]
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.