Best Practices Guide

Production-ready patterns for using Mailcraft in real applications.

Table of Contents


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

See Also