← All @molecule/* packages · App templates

@molecule/api-emails-resend

Provider bond · emails · API (Node) · v1.0.0 · Apache-2.0

Resend transactional email provider

npm install @molecule/api-emails-resend

npm · 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

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.ts JSDoc, not this file.

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-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 {@link 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

Runtime Dependencies

  • @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 official resend SDK can be swapped in later without changing the bond's interface.

  • from MUST be on a domain you VERIFIED in Resend (Resend → Domains, DNS records added and checked), or the API rejects the send with a 403 validation_error ("domain is not verified"). Before any domain is verified the only usable sender is onboarding@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: set RESEND_FROM (e.g. Acme <no-reply@your-verified-domain.com>) and it is used whenever a message's from is empty; if both are missing, sendMail() throws a tagged config-missing error naming RESEND_FROM.

  • Configuration is lazy and env-driven: RESEND_API_KEY (and the optional RESEND_BASE_URL override for brokers / compatible endpoints) are read on EACH send — never at import time — so a key resolved into process.env after 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) naming RESEND_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 recipients delivered@resend.dev, bounced@resend.dev and complained@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 cid becomes Resend's content_id for inline images (<img src="cid:...">).

  • API errors surface as ResendApiError carrying status (HTTP) and code (Resend's error name — validation_error, rate_limit_exceeded, daily_quota_exceeded, …). They are deliberately NOT tagged with statusCode / errorKey, so the API middleware returns its generic 500 instead of echoing Resend's status to the caller. On success accepted echoes every to recipient (Resend returns no per-recipient verdict) and messageId is the id from 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 undefined placeholders).
  • 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.