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
- accept a transport-specific configuration object
- connect to the delivery provider
- send the formatted message
- return a
messageIdwhen the provider supplies one - 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