form-mailerLightweight form delivery

How-To: Implement an Adapter

This guide walks through the shape of a transport adapter for form-mailer.

For the exact adapter contract, see Reference: Adapters.

What an adapter does

An adapter is a small transport implementation that knows how to deliver an already-built email message.

In practice, the core package handles:

  • input validation described in Explanation: Validation
  • header safety
  • message assembly
  • typed success and failure results

An adapter handles delivery only.

The transport contract

A transport adapter implements a single method:

TypeScript shape:

send(message): Promise<TransportSendResult>

The exact type names and return shapes are defined in Reference: API and Reference: Adapters.

JavaScript shape:

async send(message) {
  // return { messageId: 'provider-message-id' }
}

The message already contains:

  • formatted sender and recipient addresses
  • the reply-to header if needed
  • the subject line
  • the plain text body
  • the HTML body when available

Typical adapter flow

  1. accept a transport-specific configuration object
  2. connect to the delivery provider
  3. send the formatted message
  4. return a messageId when the provider supplies one
  5. throw or reject with a transport-specific error if delivery fails

Example shape

TypeScript example:

import type { MailTransport, OutgoingMail, TransportSendResult } from '@greyharbor/form-mailer';

export function createExampleTransport(apiKey: string): MailTransport {
  return {
    async send(message: OutgoingMail): Promise<TransportSendResult> {
      // Deliver `message` using your provider here.
      // Return a message id if the provider gives you one.
      void apiKey;
      void message;
      return { messageId: 'provider-message-id' };
    },
  };
}

JavaScript example:

export function createExampleTransport(apiKey) {
  return {
    async send(message) {
      // Deliver `message` using your provider here.
      // Return a message id if the provider gives you one.
      void apiKey;
      void message;
      return { messageId: 'provider-message-id' };
    },
  };
}

Using the adapter

Pass the adapter into createFormMailer() as transport:

If your provider already matches the built-in HTTP transport shape, you can use createHttpTransport() directly instead of writing a custom adapter.

If the provider expects a different JSON shape, you can still use createHttpTransport() with mapRequest and parseResponse before dropping down to a fully custom transport.

TypeScript example:

import { createFormMailer } from '@greyharbor/form-mailer';
import { createExampleTransport } from './example-transport.js';

const mailer = createFormMailer({
  from: '[email protected]',
  to: ['[email protected]'],
  transport: createExampleTransport(process.env.EXAMPLE_API_KEY ?? ''),
});

JavaScript example:

import { createFormMailer } from '@greyharbor/form-mailer';
import { createExampleTransport } from './example-transport.js';

const mailer = createFormMailer({
  from: '[email protected]',
  to: ['[email protected]'],
  transport: createExampleTransport(process.env.EXAMPLE_API_KEY ?? ''),
});

Implementation tips

  • keep the adapter small and provider-focused
  • do not re-implement validation in the adapter
  • trust only the message shape already produced by form-mailer
  • prefer typed errors for transport failures when possible
  • keep provider-specific config out of the core package

If you want the reasoning behind that separation, read Explanation: Adapters.

Testing

At minimum, test that the adapter:

  • sends a valid message
  • surfaces provider failures clearly
  • returns a message id when available
  • handles missing required provider config cleanly