Best Practices Guide
Production-ready patterns for using Mailcraft in real applications.
Table of Contents
- Error Handling
- Concurrency and Rate Limits
- Email Deliverability
- Performance Optimization
- Security
- Testing
- Monitoring and Observability
Error Handling
Typed Error Recovery
Always handle different error types appropriately:
import {
MailcraftValidationError,
MailcraftRenderError,
MailcraftSendError
} from "@mailcraft/sdk";
async function sendWithFallback(
archetype: string,
input: any,
adapter: SenderAdapter
) {
try {
return await mailcraft.send(archetype, input, adapter);
} catch (error) {
if (error instanceof MailcraftValidationError) {
// Client error - fix input before retrying
logger.warn(`Validation failed: ${error.message}`);
throw error; // Don't retry
}
if (error instanceof MailcraftRenderError) {
// Template rendering error - likely a bug
logger.error(`Rendering failed for ${error.archetype}`, error);
throw error; // Don't retry
}
if (error instanceof MailcraftSendError) {
// Provider error - may be recoverable
const { provider, payload } = error;
if (payload.code === 429) {
// Rate limited - use exponential backoff
logger.warn(`Rate limited by ${provider}, retrying...`);
await sleep(exponentialBackoff(attempt));
return sendWithFallback(archetype, input, adapter);
}
if (payload.code === 503) {
// Service unavailable - retry with backoff
logger.warn(`${provider} unavailable, retrying...`);
await sleep(exponentialBackoff(attempt));
return sendWithFallback(archetype, input, adapter);
}
if (payload.code === 401 || payload.code === 403) {
// Auth error - don't retry
logger.error(`Auth failed with ${provider}`, error);
throw error;
}
// Other errors - log and don't retry
logger.error(`${provider} error: ${payload.reason}`, error);
throw error;
}
// Unknown error
logger.error("Unknown error sending email", error);
throw error;
}
}
Retry Strategy
Implement exponential backoff for transient failures:
import pRetry from "p-retry";
async function sendWithRetry(
archetype: string,
input: any,
adapter: SenderAdapter,
maxAttempts = 3
) {
return pRetry(
async () => {
try {
return await mailcraft.send(archetype, input, adapter);
} catch (error) {
// Only retry on transient errors
if (
error instanceof MailcraftSendError &&
[429, 500, 502, 503, 504].includes(error.payload.code || 0)
) {
throw error; // Will be retried
}
throw new pRetry.AbortError(error);
}
},
{
retries: maxAttempts,
minTimeout: 100,
maxTimeout: 30000,
factor: 2,
onFailedAttempt: (error) => {
logger.warn(
`Attempt ${error.attemptNumber} failed. ` +
`${error.retriesLeft} retries left.`
);
}
}
);
}
Concurrency and Rate Limits
Rate Limit Aware Sending
import PQueue from "p-queue";
// Respect provider rate limits (Resend: 100 per second)
const queue = new PQueue({ concurrency: 50, interval: 1000, intervalCap: 100 });
async function batchSend(
recipients: string[],
archetype: string,
makeInput: (recipient: string) => any
) {
const results = await Promise.allSettled(
recipients.map((recipient) =>
queue.add(() =>
mailcraft
.send(archetype, makeInput(recipient), adapter)
.then((result) => ({ recipient, success: true, result }))
.catch((error) => ({ recipient, success: false, error }))
)
)
);
// Aggregate results
const successful = results.filter(
(r) => r.status === "fulfilled" && r.value.success
);
const failed = results.filter(
(r) => r.status === "fulfilled" && !r.value.success
);
logger.info(
`Batch complete: ${successful.length} sent, ${failed.length} failed`
);
return { successful, failed };
}
Streaming Large Batches
For very large recipient lists, stream sends to manage memory:
import { Readable } from "stream";
import { pipeline } from "stream/promises";
async function* sendStream(recipientStream: AsyncIterable<string>) {
for await (const recipient of recipientStream) {
try {
const result = await mailcraft.send(
"newsletter",
{ to: [recipient], content: "..." },
adapter
);
yield { recipient, success: true, result };
} catch (error) {
yield { recipient, success: false, error };
}
}
}
// Usage with rate limiting
const recipients = readRecipientStream(); // AsyncIterable<string>
for await (const result of sendStream(recipients)) {
// Process as they complete
if (result.success) {
await trackSent(result.recipient);
} else {
await trackFailed(result.recipient, result.error);
}
}
Email Deliverability
Domain Reputation
Maintain sender reputation:
// 1. Use authenticated sending domain
const brand: BrandProfile = {
sender: {
fromEmail: "noreply@yourdomain.com", // Must be verified with provider
fromName: "Your Company"
}
// ... rest of brand
};
// 2. Use reply-to address for responses
const brand: BrandProfile = {
sender: {
fromEmail: "noreply@yourdomain.com",
replyTo: "support@yourdomain.com" // Allows replies to support
}
// ... rest of brand
};
Unsubscribe Headers
Always include unsubscribe URL in footer:
const brand: BrandProfile = {
footer: {
unsubscribeUrl: "https://yourdomain.com/unsubscribe?token={{user_id}}",
supportEmail: "support@yourdomain.com"
}
// ... rest of brand
};
DKIM/SPF/DMARC
Let your email provider handle authentication, but verify your domain setup:
- Resend: Domain verification via DNS records
- SES: DKIM signing automatically enabled, verify via SNS notifications
Performance Optimization
Cache Rendered Templates
Pre-render common emails:
import NodeCache from "node-cache";
const templateCache = new NodeCache({ stdTTL: 3600 }); // 1 hour
async function sendWithCache(
archetype: string,
input: any,
adapter: SenderAdapter
) {
// Only cache when input is stable (no user-specific data)
const cacheKey = `${archetype}:${JSON.stringify(input)}`;
let rendered = templateCache.get(cacheKey);
if (!rendered) {
rendered = mailcraft.render(archetype, input);
templateCache.set(cacheKey, rendered);
}
return adapter.send({
...rendered,
from: brand.sender.fromEmail
});
}
Lazy Load Adapters
Initialize adapters only when needed:
let resendAdapter: ResendAdapter | null = null;
function getResendAdapter(): ResendAdapter {
if (!resendAdapter) {
resendAdapter = new ResendAdapter(process.env.RESEND_API_KEY);
}
return resendAdapter;
}
// Usage
const result = await mailcraft.send(archetype, input, getResendAdapter());
Async Rendering
Separate rendering from I/O operations:
// Render synchronously
const rendered = mailcraft.render(archetype, input);
// Send asynchronously in background
process.nextTick(async () => {
try {
await adapter.send({
...rendered,
from: brand.sender.fromEmail
});
} catch (error) {
logger.error("Background send failed", error);
}
});
Security
Validate Email Addresses
import { isEmail } from "validator";
function validateInput(input: any): boolean {
if (!Array.isArray(input.to)) return false;
return input.to.every((email: string) => isEmail(email));
}
try {
if (!validateInput(input)) {
throw new Error("Invalid recipient email addresses");
}
await mailcraft.send(archetype, input, adapter);
} catch (error) {
logger.error("Validation failed", error);
}
Prevent Template Injection
Never interpolate user input into HTML:
// ❌ BAD - User input directly in HTML
const template = `Hello ${userName}, click here!`;
// ✅ GOOD - Use archetype inputs
mailcraft.render("welcome", {
to: [email],
title: userName, // Safely escaped by Mailcraft
body: "Welcome to our service"
});
Protect API Keys
// ❌ BAD - Hardcoded keys
const adapter = new ResendAdapter("re_xxx");
// ✅ GOOD - Use environment variables
const adapter = new ResendAdapter(process.env.RESEND_API_KEY!);
// ✅ GOOD - Use secrets manager
import SecretsManager from "aws-sdk/clients/secretsmanager";
const secret = await secretsManager
.getSecretValue({ SecretId: "resend-api-key" })
.promise();
const adapter = new ResendAdapter(secret.SecretString);
Rate Limit Recipient Input
function validateBatchSize(recipients: string[]): void {
const MAX_BATCH = 10000; // Reasonable limit
if (recipients.length > MAX_BATCH) {
throw new Error(`Batch too large: ${recipients.length} > ${MAX_BATCH}`);
}
}
Testing
Unit Testing with Mock Adapter
import { describe, it, expect } from "vitest";
describe("Email sending", () => {
it("renders welcome email with correct content", () => {
const rendered = mailcraft.render("welcome", {
to: ["user@example.com"],
title: "Welcome!",
body: "Get started here"
});
expect(rendered.to).toEqual(["user@example.com"]);
expect(rendered.html).toContain("Welcome!");
expect(rendered.text).toContain("Get started here");
});
it("sends email with mock adapter", async () => {
const mockAdapter = new ResendAdapter(); // No API key = mock mode
const result = await mailcraft.send(
"verification",
{
to: ["user@example.com"],
code: "123456"
},
mockAdapter
);
expect(result.provider).toBe("resend");
expect(result.metadata?.messageId).toBeDefined();
});
});
Integration Testing
import { afterEach, describe, it, expect, vi } from "vitest";
describe("Email service integration", () => {
const adapter = new ResendAdapter(process.env.RESEND_API_KEY);
afterEach(() => {
vi.clearAllMocks();
});
it("sends real email when API key is available", async () => {
// Only run with valid API key
if (!process.env.RESEND_API_KEY) {
vi.skip();
}
const result = await mailcraft.send(
"welcome",
{
to: ["test@example.com"],
title: "Integration Test",
body: "This is a test"
},
adapter
);
expect(result.id).toBeDefined();
expect(result.provider).toBe("resend");
});
});
Monitoring and Observability
Structured Logging
import winston from "winston";
const logger = winston.createLogger({
format: winston.format.json(),
defaultMeta: { service: "email-service" },
transports: [new winston.transports.Console()]
});
async function loggedSend(
archetype: string,
input: any,
adapter: SenderAdapter
) {
const startTime = Date.now();
try {
const result = await mailcraft.send(archetype, input, adapter);
logger.info("Email sent successfully", {
archetype,
recipients: input.to,
provider: result.provider,
messageId: result.metadata?.messageId,
duration: Date.now() - startTime
});
return result;
} catch (error) {
logger.error("Email send failed", {
archetype,
recipients: input.to,
error:
error instanceof Error
? {
message: error.message,
code:
error instanceof MailcraftSendError
? error.payload.code
: undefined
}
: String(error),
duration: Date.now() - startTime
});
throw error;
}
}
Metrics Collection
import { StatsD } from "node-statsd";
const statsd = new StatsD();
async function metricsSend(
archetype: string,
input: any,
adapter: SenderAdapter
) {
const startTime = Date.now();
try {
const result = await mailcraft.send(archetype, input, adapter);
statsd.timing(`email.${archetype}.success`, Date.now() - startTime);
statsd.increment(`email.${archetype}.sent`);
return result;
} catch (error) {
statsd.timing(`email.${archetype}.failure`, Date.now() - startTime);
statsd.increment(`email.${archetype}.failed`);
if (error instanceof MailcraftSendError) {
statsd.increment(`email.provider.${error.provider}.failed`);
}
throw error;
}
}
Alerting
async function alertingSend(
archetype: string,
input: any,
adapter: SenderAdapter,
alertManager: AlertManager
) {
try {
return await mailcraft.send(archetype, input, adapter);
} catch (error) {
// Alert on critical failures
if (error instanceof MailcraftSendError) {
if (error.payload.code === 401 || error.payload.code === 403) {
// Critical: auth failure
await alertManager.critical(
`Email provider auth failure: ${error.payload.reason}`
);
} else if ([500, 502, 503, 504].includes(error.payload.code || 0)) {
// Warning: service degradation
await alertManager.warning(
`Email provider ${error.provider} returned ${error.payload.code}`
);
}
}
throw error;
}
}
Summary Checklist
- ✅ Handle all three error types appropriately
- ✅ Implement exponential backoff for transient failures
- ✅ Respect provider rate limits with queue
- ✅ Use verified domain as sender
- ✅ Include unsubscribe link in footer
- ✅ Cache rendered templates for performance
- ✅ Validate email addresses and input
- ✅ Never hardcode API keys
- ✅ Implement structured logging
- ✅ Set up metrics and alerting
- ✅ Test with mock adapter in CI
- ✅ Monitor production sends