@molecule/api-emails-resend

Provider bond · emails · API (Node) · v1.0.2 · 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

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

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 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.