Developer GuideBest Practices

Best Practices

Security, performance, and optimization tips for using the FormaMail API.

Overview

Follow these best practices to build secure, reliable, and performant email integrations with FormaMail. These guidelines cover API key security, error handling, performance optimization, and email delivery best practices.

Important: These best practices apply specifically to API usage. For template design and dashboard features, see the User Guide.


API Key Security

1. Never Hardcode API Keys

❌ Bad:

const apiKey = 'fm_sk_abc123xyz456...'; // Hardcoded - dangerous!
 
fetch('https://api.formamail.com/api/emails/send', {
  headers: { 'Authorization': `Bearer ${apiKey}` }
});

✅ Good:

const apiKey = process.env.FORMAMAIL_API_KEY; // From environment variable
 
fetch('https://api.formamail.com/api/emails/send', {
  headers: { 'Authorization': `Bearer ${apiKey}` }
});

2. Use Environment Variables

Store API keys in environment variables, never in code:

.env file:

FORMAMAIL_API_KEY=fm_sk_abc123xyz456...

.gitignore:

.env
.env.local
.env.production

Node.js:

require('dotenv').config();
const apiKey = process.env.FORMAMAIL_API_KEY;

Python:

import os
api_key = os.environ.get('FORMAMAIL_API_KEY')

PHP:

$apiKey = getenv('FORMAMAIL_API_KEY');

3. Use Separate Keys for Different Environments

Create separate API keys for development, staging, and production:

DEV_FORMAMAIL_API_KEY=fm_sk_dev123...
STAGING_FORMAMAIL_API_KEY=fm_sk_staging456...
PROD_FORMAMAIL_API_KEY=fm_sk_prod789...

Benefits:

  • Isolate test traffic from production
  • Revoke dev keys without affecting production
  • Monitor usage separately

4. Rotate Keys Regularly

Rotate API keys every 90 days:

// Store key creation date in your database
const KEY_AGE_LIMIT_DAYS = 90;
 
function checkKeyAge(createdAt) {
  const ageInDays = (Date.now() - createdAt) / (1000 * 60 * 60 * 24);
 
  if (ageInDays > KEY_AGE_LIMIT_DAYS) {
    console.warn('API key is old! Rotate immediately.');
    // Alert admin, create new key in dashboard
  }
}

5. Never Log API Keys

❌ Bad:

console.log('Sending with key:', apiKey); // Logs sensitive data!
logger.debug({ apiKey, templateId }); // Exposes key in logs!

✅ Good:

console.log('Sending email...'); // No sensitive data
logger.debug({ templateId, recipientCount }); // Safe information only
 
// If you must log for debugging, redact the key
const redactedKey = apiKey.substring(0, 10) + '***';
console.log('Using key:', redactedKey); // Safe

6. Restrict API Key Permissions

When creating API keys in the dashboard:

  • ✅ Only grant necessary permissions (email sending only)
  • ✅ Set expiration dates
  • ✅ Name keys descriptively (“Production Server”, “Staging API”)
  • ❌ Don’t create overly permissive keys

Error Handling

1. Always Check Response Status

❌ Bad:

const response = await fetch(url, options);
const data = await response.json(); // Might fail if status is 4xx/5xx

✅ Good:

const response = await fetch(url, options);
 
if (!response.ok) {
  const error = await response.json();
  throw new Error(`API Error: ${error.code} - ${error.message}`);
}
 
const data = await response.json();

2. Implement Retry Logic with Exponential Backoff

✅ Production-ready retry logic:

async function sendEmailWithRetry(emailData, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch('https://api.formamail.com/api/emails/send', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.FORMAMAIL_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(emailData)
      });
 
      // Handle rate limiting
      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
        console.log(`Rate limited. Retrying in ${retryAfter}s...`);
        await sleep(retryAfter * 1000);
        continue;
      }
 
      // Handle server errors (5xx) - retry
      if (response.status >= 500) {
        const delay = Math.min(2 ** attempt * 1000, 30000); // Max 30s
        console.log(`Server error. Retrying in ${delay}ms...`);
        await sleep(delay);
        continue;
      }
 
      // Handle client errors (4xx) - don't retry
      if (response.status >= 400) {
        const error = await response.json();
        throw new Error(`${error.code}: ${error.message}`);
      }
 
      // Success
      return await response.json();
    } catch (error) {
      // Last attempt - throw error
      if (attempt === maxRetries - 1) {
        throw error;
      }
 
      // Network error - retry with exponential backoff
      const delay = Math.min(2 ** attempt * 1000, 30000);
      console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
      await sleep(delay);
    }
  }
}
 
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

3. Handle Specific Error Codes

async function handleApiError(error) {
  switch (error.code) {
    case 'ERR_AUTH_001':
      // Invalid API key - check key, don't retry
      console.error('Invalid API key. Check FORMAMAIL_API_KEY environment variable.');
      alertAdmin('API key invalid or expired');
      break;
 
    case 'ERR_TMPL_002':
      // Template not found - check template ID, don't retry
      console.error(`Template not found: ${error.relatedInfo.templateId}`);
      break;
 
    case 'ERR_QUOTA_001':
      // Email quota exceeded - wait until next billing period
      console.error('Monthly email quota exceeded');
      alertAdmin('Upgrade plan or wait for quota reset');
      break;
 
    case 'ERR_QUOTA_003':
      // Rate limit - retry after specified time
      const retryAfter = error.relatedInfo.retryAfterSeconds;
      console.log(`Rate limited. Retry after ${retryAfter}s`);
      await sleep(retryAfter * 1000);
      return sendEmail(emailData); // Retry once
      break;
 
    default:
      // Unknown error - log and alert
      console.error('Unexpected error:', error);
      alertAdmin(`API error: ${error.code}`);
  }
}

4. Log Errors Properly

✅ Structured logging:

function logApiError(error, context) {
  logger.error({
    timestamp: new Date().toISOString(),
    errorCode: error.code,
    errorMessage: error.message,
    statusCode: error.statusCode,
    context: {
      templateId: context.templateId,
      recipientCount: context.recipientCount,
      // Don't log recipient emails (privacy)
    },
    stack: error.stack
  });
}

Performance Optimization

1. Use Bulk Endpoints

❌ Bad - 100 individual requests:

for (const recipient of recipients) {
  await sendEmail({
    templateId: 'tmpl_welcome',
    to: [{ email: recipient.email }],
    variables: recipient.variables
  });
}
// 100 API calls, 100x the time, 100x rate limit usage

✅ Good - 1 bulk request:

await sendBulkEmails({
  templateId: 'tmpl_welcome',
  recipients: recipients.map(r => ({
    email: r.email,
    variables: r.variables
  }))
});
// 1 API call, much faster, efficient rate limit usage

2. Implement Request Queuing

For high-volume applications, queue requests to avoid rate limits:

class EmailQueue {
  constructor(rateLimitPerMinute = 30) {
    this.queue = [];
    this.processing = false;
    this.rateLimitPerMinute = rateLimitPerMinute;
    this.requestTimes = [];
  }
 
  enqueue(emailData) {
    return new Promise((resolve, reject) => {
      this.queue.push({ emailData, resolve, reject });
      this.process();
    });
  }
 
  async process() {
    if (this.processing || this.queue.length === 0) return;
    this.processing = true;
 
    while (this.queue.length > 0) {
      // Clean up old request times (>1 minute ago)
      const now = Date.now();
      this.requestTimes = this.requestTimes.filter(t => now - t < 60000);
 
      // Wait if at rate limit
      if (this.requestTimes.length >= this.rateLimitPerMinute) {
        const oldestRequest = this.requestTimes[0];
        const waitTime = 60000 - (now - oldestRequest);
        await sleep(waitTime);
        continue;
      }
 
      // Process next email
      const { emailData, resolve, reject } = this.queue.shift();
      this.requestTimes.push(now);
 
      try {
        const result = await sendEmail(emailData);
        resolve(result);
      } catch (error) {
        reject(error);
      }
    }
 
    this.processing = false;
  }
}
 
// Usage
const queue = new EmailQueue(30);
const result = await queue.enqueue(emailData);

3. Validate Before Sending

Validate data before making API calls to avoid wasted requests:

function validateEmailData(data) {
  // Check required fields
  if (!data.templateId) {
    throw new Error('Template ID required');
  }
 
  if (!data.to || data.to.length === 0) {
    throw new Error('At least one recipient required');
  }
 
  // Validate email addresses
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  for (const recipient of data.to) {
    if (!emailRegex.test(recipient.email)) {
      throw new Error(`Invalid email: ${recipient.email}`);
    }
  }
 
  // Validate required variables
  if (!data.variables) {
    throw new Error('Variables object required');
  }
 
  return true;
}
 
// Use it
try {
  validateEmailData(emailData); // Validate first
  const result = await sendEmail(emailData); // Then send
} catch (error) {
  console.error('Validation error:', error.message);
}

4. Cache Template IDs

Store template IDs in your application configuration instead of fetching them repeatedly:

// config/email-templates.js
const EMAIL_TEMPLATES = {
  WELCOME: process.env.TEMPLATE_ID_WELCOME || 'tmpl_welcome_abc123',
  PASSWORD_RESET: process.env.TEMPLATE_ID_PASSWORD_RESET || 'tmpl_reset_xyz456',
  INVOICE: process.env.TEMPLATE_ID_INVOICE || 'tmpl_invoice_def789',
  NEWSLETTER: process.env.TEMPLATE_ID_NEWSLETTER || 'tmpl_newsletter_ghi012',
};
 
// Usage
await sendEmail({
  templateId: EMAIL_TEMPLATES.WELCOME,
  to: [{ email: user.email }],
  variables: { ... }
});

Note: Templates are managed in the FormaMail dashboard. Store template IDs in environment variables for easy configuration across environments.


Email Delivery Best Practices

1. Always Use Templates

FormaMail requires templates for all emails:

✅ Templates ensure:

  • Consistent branding
  • Variable validation
  • Reusability
  • Easy updates without code changes

Create templates in dashboard, reference via API:

{
  templateId: 'tmpl_welcome',
  to: [{ email: 'user@example.com' }],
  variables: {
    firstName: 'John',
    companyName: 'Acme Corp'
  }
}

2. Use Descriptive Subject Lines

// ❌ Bad
{ subject: 'Update' }
 
// ✅ Good
{ subject: 'Your Acme Corp invoice is ready' }

3. Provide Value in Variables

// ❌ Bad - minimal personalization
{
  variables: {
    name: 'User'
  }
}
 
// ✅ Good - rich personalization
{
  variables: {
    firstName: 'John',
    lastName: 'Doe',
    companyName: 'Acme Corp',
    accountTier: 'Pro',
    dashboardUrl: 'https://app.acme.com/dashboard',
    supportEmail: 'support@acme.com'
  }
}

4. Bounce and Complaint Handling

FormaMail automatically tracks email delivery events and manages suppression lists:

What’s Automated:

  • ✅ Hard bounces tracked and suppressed automatically
  • ✅ Spam complaints tracked and suppressed immediately
  • ✅ Suppressed emails filtered out before sending
  • ✅ Delivery, bounce, and complaint tracking via dashboard

Your Responsibility:

  • Maintain clean email lists in your database
  • Remove invalid emails from your mailing lists
  • Monitor bounce/complaint rates in the dashboard
  • Follow email best practices (permission-based sending, unsubscribe links)

Healthy Metrics (view in dashboard):

  • Bounce rate: < 5%
  • Complaint rate: < 0.1%

Automatic Protection: FormaMail prevents you from sending to bounced or complained addresses. If all recipients are suppressed, the API returns an error with details. If some are suppressed, they’re automatically filtered out and the email sends to valid recipients only.


Testing

1. Use Test API Keys in Development

const apiKey = process.env.NODE_ENV === 'production'
  ? process.env.FORMAMAIL_API_KEY_PROD
  : process.env.FORMAMAIL_API_KEY_TEST;

Benefits:

  • No cost during development
  • Isolated from production metrics
  • Can delete/reset without risk

2. Test with Real Data

✅ Production-like test data:

const testEmail = {
  templateId: 'tmpl_welcome',
  to: [{ email: 'test@yourdomain.com', name: 'Test User' }],
  variables: {
    firstName: 'John',
    lastName: 'Doe',
    companyName: 'Test Corp',
    accountUrl: 'https://staging.app.com/account/test-123',
    supportEmail: 'support@yourdomain.com'
  }
};

3. Test Error Scenarios

// Test with invalid template ID
await expect(sendEmail({ templateId: 'invalid' }))
  .rejects.toThrow('ERR_TMPL_002');
 
// Test with missing required variable
await expect(sendEmail({
  templateId: 'tmpl_welcome',
  variables: {} // Missing required variables
})).rejects.toThrow('ERR_TMPL_004');
 
// Test rate limiting
const promises = Array(35).fill().map(() => sendEmail(testEmail));
const results = await Promise.allSettled(promises);
const rateLimited = results.filter(r => r.reason?.code === 'ERR_QUOTA_003');
expect(rateLimited.length).toBeGreaterThan(0);

4. Test Email Rendering

Send test emails to yourself before production:

if (process.env.NODE_ENV !== 'production') {
  // Override recipient in dev/staging
  emailData.to = [{ email: 'developer@yourdomain.com' }];
}

Monitoring and Logging

1. Log All Email Sends

async function sendEmail(emailData) {
  const startTime = Date.now();
 
  try {
    const result = await fetch(url, options);
 
    // Log success
    logger.info({
      event: 'email_sent',
      emailId: result.id,
      templateId: emailData.templateId,
      recipientCount: emailData.to.length,
      duration: Date.now() - startTime,
      timestamp: new Date().toISOString()
    });
 
    return result;
  } catch (error) {
    // Log failure
    logger.error({
      event: 'email_failed',
      templateId: emailData.templateId,
      errorCode: error.code,
      errorMessage: error.message,
      duration: Date.now() - startTime,
      timestamp: new Date().toISOString()
    });
 
    throw error;
  }
}

2. Monitor API Health

async function checkApiHealth() {
  try {
    const response = await fetch('https://api.formamail.com/api/health');
 
    if (!response.ok) {
      alertAdmin('FormaMail API is down!');
    }
  } catch (error) {
    alertAdmin('Cannot reach FormaMail API');
  }
}
 
// Check every 5 minutes
setInterval(checkApiHealth, 300000);

3. Track Success Rates

const metrics = {
  sent: 0,
  failed: 0,
  rateLimited: 0
};
 
async function sendEmailWithMetrics(emailData) {
  try {
    const result = await sendEmail(emailData);
    metrics.sent++;
    return result;
  } catch (error) {
    if (error.code === 'ERR_QUOTA_003') {
      metrics.rateLimited++;
    } else {
      metrics.failed++;
    }
    throw error;
  }
}
 
// Report metrics every hour
setInterval(() => {
  logger.info({
    event: 'email_metrics',
    sent: metrics.sent,
    failed: metrics.failed,
    rateLimited: metrics.rateLimited,
    successRate: (metrics.sent / (metrics.sent + metrics.failed)) * 100
  });
 
  // Reset counters
  metrics.sent = 0;
  metrics.failed = 0;
  metrics.rateLimited = 0;
}, 3600000);

4. Alert on Critical Errors

function alertAdmin(message, severity = 'warning') {
  // Send to monitoring service (e.g., Sentry, DataDog, PagerDuty)
  if (severity === 'critical') {
    // Page on-call engineer
    sendSlackAlert(`🚨 CRITICAL: ${message}`);
    sendPagerDutyAlert(message);
  } else if (severity === 'warning') {
    // Log and notify
    sendSlackAlert(`⚠️ WARNING: ${message}`);
  }
 
  logger.log(severity, message);
}

Security Considerations

1. Sanitize User Input

Always sanitize data before including in emails:

function sanitizeInput(input) {
  if (typeof input !== 'string') return input;
 
  // Remove potentially dangerous content
  return input
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    .replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
    .replace(/javascript:/gi, '');
}
 
// Use it
const emailData = {
  templateId: 'tmpl_welcome',
  to: [{ email: userEmail }],
  variables: {
    firstName: sanitizeInput(user.firstName),
    companyName: sanitizeInput(user.companyName),
    // ... sanitize all user-provided data
  }
};

2. Validate Email Addresses

function isValidEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 
  if (!regex.test(email)) return false;
 
  // Additional checks
  if (email.length > 254) return false; // RFC 5321
  if (email.split('@')[0].length > 64) return false; // Local part max
 
  return true;
}
 
// Use it
if (!isValidEmail(userEmail)) {
  throw new Error('Invalid email address');
}

3. Implement Rate Limiting on Your End

Even though FormaMail has rate limits, add your own to prevent abuse:

const userRequestCounts = new Map();
 
function checkUserRateLimit(userId, maxPerHour = 100) {
  const now = Date.now();
  const oneHourAgo = now - 3600000;
 
  // Get user's request history
  let requests = userRequestCounts.get(userId) || [];
 
  // Remove requests older than 1 hour
  requests = requests.filter(time => time > oneHourAgo);
 
  // Check limit
  if (requests.length >= maxPerHour) {
    throw new Error('User rate limit exceeded');
  }
 
  // Record request
  requests.push(now);
  userRequestCounts.set(userId, requests);
}

Production Checklist

Before deploying to production, verify:

API Configuration

  • ✅ API key stored in environment variable (not hardcoded)
  • ✅ Using production API key (fm_sk_...)
  • ✅ API key has appropriate permissions
  • ✅ API key expiration date set (if applicable)

Error Handling

  • ✅ Retry logic implemented with exponential backoff
  • ✅ All error codes handled appropriately
  • ✅ Errors logged with structured logging
  • ✅ Critical errors trigger alerts

Performance

  • ✅ Using bulk endpoints for multiple emails
  • ✅ Request queuing implemented for high volume
  • ✅ Rate limit headers monitored
  • ✅ Input validation before API calls

Security

  • ✅ User input sanitized
  • ✅ Email addresses validated
  • ✅ API keys not logged
  • ✅ HTTPS enforced for all API calls

Monitoring

  • ✅ All email sends logged
  • ✅ Success/failure metrics tracked
  • ✅ Health checks configured
  • ✅ Alerts set up for critical errors

Testing

  • ✅ Tested with production-like data
  • ✅ Error scenarios tested
  • ✅ Rate limiting tested
  • ✅ Email rendering verified

Common Pitfalls

1. Not Handling Rate Limits

❌ Problem:

for (const user of users) {
  await sendEmail({ ... }); // Will hit rate limit quickly
}

✅ Solution: Use bulk endpoints or implement queuing (see Performance Optimization).

2. No Retry Logic

❌ Problem: Single network error causes email to fail permanently.

✅ Solution: Implement retry with exponential backoff (see Error Handling).

3. Hardcoded Template IDs

❌ Problem:

const TEMPLATE_ID = 'tmpl_abc123'; // Hardcoded

✅ Solution:

const TEMPLATE_ID = process.env.WELCOME_EMAIL_TEMPLATE_ID;

4. Not Validating Before Sending

❌ Problem: Wasting API calls on invalid data.

✅ Solution: Validate email addresses, required fields, and variable types before API calls.


Next Steps


Related: Sending Emails | Error Handling | Rate Limiting | Authentication