@molecule/api-emails-sendgrid

Provider bond · email · API (Node) · v1.0.3 · Apache-2.0

SendGrid email provider for molecule.dev.

npm install @molecule/api-emails-sendgrid

npm · Source on GitHub · Implements @molecule/api-emails

How it works

@molecule/api-emails-sendgrid is a provider bond on the API (Node) side: it implements the email 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-sendgrid'

setTransport(provider)

Works with: @molecule/api-bond, @molecule/api-emails, @molecule/api-secrets

Secrets: SENDGRID_API_KEY

Reference

SendGrid 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-sendgrid'

setTransport(provider)

Type

provider

Installation

npm install @molecule/api-emails-sendgrid @molecule/api-analytics @molecule/api-bond @molecule/api-emails @molecule/api-secrets @sendgrid/mail

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

Functions

sendMail(message)

Sends an email through the SendGrid 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 SendGrid mail client, applying configuration from the environment on FIRST USE and memoizing each setting thereafter.

Configuration is deferred to the first send — NOT module load — so an app that resolves SENDGRID_API_KEY (and the optional SENDGRID_BASE_URL) into process.env AFTER this module is imported (late secrets resolution via a secrets bond) is honored: the value present at send time is the one applied. Reading the key at import time instead froze an empty/stale key and every request went out unauthenticated — an opaque SendGrid 401. Each env var is applied once, the first time it is seen set, so whichever arrives late is still picked up.

function getClient(): sgMail.MailService

Returns: The configured @sendgrid/mail client.

Constants

provider

The SendGrid email provider implementing the standard interface.

const provider: EmailTransport

emailsSendgridSecretDefinitions

Secret definitions required by the SendGrid email bond.

const emailsSendgridSecretDefinitions: SecretDefinition[]

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-sendgrid'

export function setupEmailsSendgrid(): 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

  • SENDGRID_API_KEY (required) — SendGrid API key

Runtime Dependencies

  • @molecule/api-analytics

  • @molecule/api-bond

  • @molecule/api-emails

  • @molecule/api-secrets

  • @sendgrid/mail

  • Configuration is lazy and env-driven: SENDGRID_API_KEY (and the optional SENDGRID_BASE_URL) are read on the FIRST send via getClient() — NOT at import time — and applied once. So a key resolved into process.env AFTER this module is imported (late secrets resolution via a secrets bond) is honored: the value present at send time is the one used. If the key is genuinely absent at send time, sendMail() throws a tagged config-missing error (clean 503 / config.notConfigured) naming SENDGRID_API_KEY — never an opaque SendGrid 401.

  • SENDGRID_TEST_MODE=true enables SendGrid sandbox mode: the API validates and accepts the message (auth + payload exercised for real) but NOTHING is delivered. SENDGRID_BASE_URL (optional, read lazily on first send too) overrides the API base URL for brokers/compatible endpoints.

  • Stream attachments are not supported — Buffer/string content only; a stream throws. On success accepted echoes every to recipient (SendGrid returns no per-recipient verdict) and messageId is taken from the x-message-id response header.

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.