API integration done right accelerates development and ensures reliable applications. This guide covers authentication strategies, error handling patterns, performance optimization, and real-world integration examples from our experience building email testing tools.
API Integration Fundamentals
RESTful API Design Principles
Understanding REST principles is essential for effective API integration:
- Resource-based URLs: /api/users/123 not /api/getUser?id=123
- HTTP methods for actions: GET (read), POST (create), PUT/PATCH (update), DELETE (remove)
- Stateless requests: Each request contains all necessary information
- Standard HTTP status codes: 200 (success), 400 (client error), 500 (server error)
- JSON format for data exchange with consistent structure
- HATEOAS: Include links to related resources in responses
API Client Architecture
// Robust API client implementation
class APIClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.timeout = options.timeout || 30000;
this.retryAttempts = options.retryAttempts || 3;
this.retryDelay = options.retryDelay || 1000;
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers
};
this.interceptors = {
request: [],
response: []
};
}
// Add request interceptor
addRequestInterceptor(interceptor) {
this.interceptors.request.push(interceptor);
}
// Add response interceptor
addResponseInterceptor(interceptor) {
this.interceptors.response.push(interceptor);
}
// Execute request with retry logic
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
let config = {
method: 'GET',
headers: { ...this.defaultHeaders },
timeout: this.timeout,
...options
};
// Apply request interceptors
for (const interceptor of this.interceptors.request) {
config = await interceptor(config);
}
let lastError;
for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
const response = await fetch(url, {
...config,
signal: controller.signal
});
clearTimeout(timeoutId);
// Apply response interceptors
let processedResponse = response;
for (const interceptor of this.interceptors.response) {
processedResponse = await interceptor(processedResponse);
}
return processedResponse;
} catch (error) {
lastError = error;
// Don't retry on client errors (4xx)
if (error.response && error.response.status >= 400 && error.response.status < 500) {
throw error;
}
// Wait before retry (exponential backoff)
if (attempt < this.retryAttempts) {
await this.delay(this.retryDelay * Math.pow(2, attempt));
}
}
}
throw lastError;
}
// Helper methods for common HTTP methods
get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, { method: 'GET' });
}
post(endpoint, data = {}) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data = {}) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}Authentication Strategies
API Key Authentication
Most common for service-to-service communication:
// API Key authentication implementation
class APIKeyAuth {
constructor(apiKey, headerName = 'X-API-Key') {
this.apiKey = apiKey;
this.headerName = headerName;
}
// Request interceptor for API key
requestInterceptor(config) {
config.headers[this.headerName] = this.apiKey;
return config;
}
}
// Usage example
const client = new APIClient('https://api.example.com');
const auth = new APIKeyAuth('your-api-key-here');
client.addRequestInterceptor(auth.requestInterceptor.bind(auth));
// Alternative: Query parameter authentication
class QueryParamAuth {
constructor(apiKey, paramName = 'api_key') {
this.apiKey = apiKey;
this.paramName = paramName;
}
requestInterceptor(config) {
const url = new URL(config.url);
url.searchParams.set(this.paramName, this.apiKey);
config.url = url.toString();
return config;
}
}Bearer Token Authentication
// Bearer token with refresh logic
class BearerTokenAuth {
constructor(tokenEndpoint, credentials) {
this.tokenEndpoint = tokenEndpoint;
this.credentials = credentials;
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiry = null;
}
// Get valid access token
async getAccessToken() {
if (!this.accessToken || this.isTokenExpired()) {
await this.refreshAccessToken();
}
return this.accessToken;
}
// Check if token is expired
isTokenExpired() {
return !this.tokenExpiry || Date.now() >= this.tokenExpiry;
}
// Refresh access token
async refreshAccessToken() {
try {
const response = await fetch(this.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
grant_type: 'client_credentials',
...this.credentials
})
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const tokenData = await response.json();
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
// Calculate expiry time (subtract buffer for safety)
const expiresIn = tokenData.expires_in || 3600;
this.tokenExpiry = Date.now() + (expiresIn - 300) * 1000;
} catch (error) {
throw new Error(`Authentication failed: ${error.message}`);
}
}
// Request interceptor
async requestInterceptor(config) {
const token = await this.getAccessToken();
config.headers['Authorization'] = `Bearer ${token}`;
return config;
}
// Response interceptor to handle auth errors
async responseInterceptor(response) {
if (response.status === 401) {
// Token expired, refresh and retry
this.accessToken = null;
throw new Error('Authentication expired');
}
return response;
}
}OAuth 2.0 Implementation
// OAuth 2.0 Authorization Code Flow
class OAuth2Client {
constructor(clientId, clientSecret, redirectUri, authEndpoint, tokenEndpoint) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectUri = redirectUri;
this.authEndpoint = authEndpoint;
this.tokenEndpoint = tokenEndpoint;
this.scopes = [];
}
// Generate authorization URL
getAuthorizationUrl(scopes = [], state = null) {
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: scopes.join(' '),
state: state || this.generateState()
});
return `${this.authEndpoint}?${params.toString()}`;
}
// Exchange authorization code for access token
async exchangeCodeForToken(code, state = null) {
try {
const response = await fetch(this.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}`
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: this.redirectUri
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
const tokenData = await response.json();
return {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
expires_in: tokenData.expires_in,
token_type: tokenData.token_type,
scope: tokenData.scope
};
} catch (error) {
throw new Error(`OAuth token exchange failed: ${error.message}`);
}
}
// Generate secure random state parameter
generateState() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
}Rate Limiting and Throttling
Client-Side Rate Limiting
// Token bucket rate limiter
class TokenBucket {
constructor(capacity, refillRate, refillPeriod = 1000) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRate;
this.refillPeriod = refillPeriod;
this.lastRefill = Date.now();
this.queue = [];
}
// Consume tokens
async consume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return Promise.resolve();
}
// Queue the request
return new Promise((resolve) => {
this.queue.push({ tokens, resolve });
this.processQueue();
});
}
// Refill tokens based on time elapsed
refill() {
const now = Date.now();
const timePassed = now - this.lastRefill;
const tokensToAdd = Math.floor((timePassed / this.refillPeriod) * this.refillRate);
if (tokensToAdd > 0) {
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
// Process queued requests
processQueue() {
while (this.queue.length > 0) {
this.refill();
const request = this.queue[0];
if (this.tokens >= request.tokens) {
this.tokens -= request.tokens;
this.queue.shift();
request.resolve();
} else {
// Wait and try again
setTimeout(() => this.processQueue(), 100);
break;
}
}
}
}
// Rate limiting API client
class RateLimitedAPIClient extends APIClient {
constructor(baseURL, options = {}) {
super(baseURL, options);
// Rate limiter configuration
const rateLimit = options.rateLimit || { capacity: 100, refillRate: 10 };
this.rateLimiter = new TokenBucket(
rateLimit.capacity,
rateLimit.refillRate,
rateLimit.refillPeriod
);
// Add rate limiting to requests
this.addRequestInterceptor(this.rateLimitInterceptor.bind(this));
}
async rateLimitInterceptor(config) {
await this.rateLimiter.consume(config.cost || 1);
return config;
}
}Handling Rate Limit Responses
// Handle rate limit responses from server
class RateLimitHandler {
constructor(client) {
this.client = client;
this.retryQueue = new Map();
}
// Response interceptor for rate limit handling
async responseInterceptor(response) {
if (response.status === 429) {
const retryAfter = this.getRetryAfter(response);
const requestId = this.generateRequestId(response.url);
// Add to retry queue
return new Promise((resolve, reject) => {
this.retryQueue.set(requestId, {
resolve,
reject,
retryAfter,
originalRequest: response.config
});
this.scheduleRetry(requestId);
});
}
return response;
}
// Extract retry-after header
getRetryAfter(response) {
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
// Could be seconds or HTTP date
const seconds = parseInt(retryAfter, 10);
return isNaN(seconds) ?
new Date(retryAfter).getTime() - Date.now() :
seconds * 1000;
}
// Fallback to exponential backoff
return Math.pow(2, this.getRetryCount(response.url)) * 1000;
}
// Schedule retry for rate-limited request
scheduleRetry(requestId) {
const request = this.retryQueue.get(requestId);
if (!request) return;
setTimeout(async () => {
try {
const response = await this.client.request(
request.originalRequest.url,
request.originalRequest
);
request.resolve(response);
} catch (error) {
request.reject(error);
} finally {
this.retryQueue.delete(requestId);
}
}, request.retryAfter);
}
generateRequestId(url) {
return `${url}-${Date.now()}`;
}
getRetryCount(url) {
// Implementation depends on how you want to track retries
return 1; // Simplified
}
}Error Handling Strategies
Comprehensive Error Classification
// API error classification and handling
class APIError extends Error {
constructor(message, status, code, details = {}) {
super(message);
this.name = 'APIError';
this.status = status;
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
this.retryable = this.isRetryable();
}
isRetryable() {
// Network errors and server errors are generally retryable
if (this.status >= 500) return true;
if (this.status === 429) return true; // Rate limited
if (this.status === 408) return true; // Timeout
if (this.status === 0) return true; // Network error
// Client errors are generally not retryable
if (this.status >= 400 && this.status < 500) {
// Exception: authentication errors might be retryable if token refresh is possible
return this.status === 401;
}
return false;
}
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
code: this.code,
details: this.details,
timestamp: this.timestamp,
retryable: this.retryable
};
}
}
// Error handler with different strategies
class APIErrorHandler {
constructor(options = {}) {
this.logErrors = options.logErrors !== false;
this.trackErrors = options.trackErrors !== false;
this.userNotification = options.userNotification || this.defaultUserNotification;
}
async handleError(error, context = {}) {
const apiError = this.normalizeError(error);
// Log error for debugging
if (this.logErrors) {
this.logError(apiError, context);
}
// Track error for analytics
if (this.trackErrors) {
this.trackError(apiError, context);
}
// Determine user-facing response
const userMessage = this.getUserMessage(apiError);
return {
error: apiError,
userMessage,
shouldRetry: apiError.retryable,
context
};
}
normalizeError(error) {
if (error instanceof APIError) {
return error;
}
// Network errors
if (error.name === 'NetworkError' || error.code === 'NETWORK_ERROR') {
return new APIError(
'Network connection failed',
0,
'NETWORK_ERROR',
{ originalError: error.message }
);
}
// Timeout errors
if (error.name === 'TimeoutError' || error.code === 'TIMEOUT') {
return new APIError(
'Request timed out',
408,
'TIMEOUT',
{ originalError: error.message }
);
}
// Generic error
return new APIError(
error.message || 'Unknown error occurred',
500,
'INTERNAL_ERROR',
{ originalError: error }
);
}
getUserMessage(apiError) {
const messages = {
400: 'Invalid request. Please check your input and try again.',
401: 'Authentication failed. Please check your credentials.',
403: 'Access denied. You do not have permission for this action.',
404: 'Resource not found. The requested item does not exist.',
408: 'Request timed out. Please try again.',
429: 'Too many requests. Please wait a moment and try again.',
500: 'Server error. We are working to fix this issue.',
503: 'Service temporarily unavailable. Please try again later.',
NETWORK_ERROR: 'Connection failed. Please check your internet connection.',
TIMEOUT: 'Request timed out. Please try again.'
};
return messages[apiError.status] ||
messages[apiError.code] ||
'An unexpected error occurred. Please try again.';
}
logError(error, context) {
console.error('API Error:', {
...error.toJSON(),
context,
stack: error.stack
});
}
trackError(error, context) {
// Send to error tracking service (Sentry, Bugsnag, etc.)
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'exception', {
description: `${error.status}: ${error.message}`,
fatal: false,
custom_parameter: JSON.stringify({
...error.toJSON(),
context
})
});
}
}
defaultUserNotification(message, type = 'error') {
// Default implementation - you might use toast notifications, etc.
console.warn('User notification:', { message, type });
}
}Performance Optimization
Request Optimization Strategies
// Request optimization and caching
class OptimizedAPIClient extends APIClient {
constructor(baseURL, options = {}) {
super(baseURL, options);
// Initialize caching
this.cache = new Map();
this.cacheTTL = options.cacheTTL || 300000; // 5 minutes default
// Request deduplication
this.pendingRequests = new Map();
// Batch processing
this.batchQueue = [];
this.batchDelay = options.batchDelay || 50;
this.maxBatchSize = options.maxBatchSize || 10;
}
// Cached GET request
async getCached(endpoint, params = {}, cacheTTL = this.cacheTTL) {
const cacheKey = this.generateCacheKey(endpoint, params);
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < cacheTTL) {
return cached.data;
}
const response = await this.get(endpoint, params);
// Cache successful responses
if (response.ok) {
const data = await response.json();
this.cache.set(cacheKey, {
data: { ...response, json: () => Promise.resolve(data) },
timestamp: Date.now()
});
return response;
}
return response;
}
// Request deduplication
async getDeduped(endpoint, params = {}) {
const requestKey = this.generateCacheKey(endpoint, params);
// If same request is already pending, return the same promise
if (this.pendingRequests.has(requestKey)) {
return this.pendingRequests.get(requestKey);
}
// Create new request
const promise = this.get(endpoint, params)
.finally(() => {
this.pendingRequests.delete(requestKey);
});
this.pendingRequests.set(requestKey, promise);
return promise;
}
// Batch multiple requests
async batch(requests) {
if (requests.length === 0) return [];
if (requests.length === 1) return [await this.request(requests[0].endpoint, requests[0].options)];
// Check if API supports batch endpoints
if (this.supportsBatch(requests)) {
return this.sendBatchRequest(requests);
}
// Parallel execution with concurrency limit
const concurrencyLimit = 5;
const results = [];
for (let i = 0; i < requests.length; i += concurrencyLimit) {
const batch = requests.slice(i, i + concurrencyLimit);
const batchPromises = batch.map(req =>
this.request(req.endpoint, req.options).catch(error => ({ error }))
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
return results;
}
// Auto-batching for similar requests
async autoBatch(endpoint, options = {}) {
return new Promise((resolve, reject) => {
this.batchQueue.push({ endpoint, options, resolve, reject });
// Process batch after delay or when max size reached
if (this.batchQueue.length >= this.maxBatchSize) {
this.processBatchQueue();
} else if (this.batchQueue.length === 1) {
setTimeout(() => this.processBatchQueue(), this.batchDelay);
}
});
}
async processBatchQueue() {
if (this.batchQueue.length === 0) return;
const queue = [...this.batchQueue];
this.batchQueue = [];
try {
const results = await this.batch(queue);
queue.forEach((request, index) => {
const result = results[index];
if (result.error) {
request.reject(result.error);
} else {
request.resolve(result);
}
});
} catch (error) {
queue.forEach(request => request.reject(error));
}
}
supportsBatch(requests) {
// Check if all requests are similar and API supports batching
const firstEndpoint = requests[0].endpoint.split('/')[0];
return requests.every(req =>
req.endpoint.split('/')[0] === firstEndpoint &&
req.options?.method === 'GET'
);
}
async sendBatchRequest(requests) {
// Implementation depends on your API's batch format
const batchPayload = {
requests: requests.map(req => ({
method: req.options?.method || 'GET',
url: req.endpoint,
params: req.options?.params
}))
};
const response = await this.post('/batch', batchPayload);
const data = await response.json();
return data.responses || [];
}
generateCacheKey(endpoint, params) {
const paramString = new URLSearchParams(params).toString();
return `${endpoint}${paramString ? `?${paramString}` : ''}`;
}
// Clear expired cache entries
clearExpiredCache() {
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
if (now - value.timestamp > this.cacheTTL) {
this.cache.delete(key);
}
}
}
}Real-World Integration Examples
Email Testing API Integration
// Complete email testing API client
class EmailTestingAPI {
constructor(apiKey, options = {}) {
this.client = new OptimizedAPIClient('https://api.whatismyip.io', {
timeout: 30000,
retryAttempts: 3,
cacheTTL: 300000, // 5 minutes for most lookups
...options
});
// Add authentication
this.client.addRequestInterceptor((config) => {
config.headers['X-API-Key'] = apiKey;
return config;
});
// Add error handling
this.errorHandler = new APIErrorHandler({
logErrors: true,
trackErrors: true
});
this.client.addResponseInterceptor(async (response) => {
if (!response.ok) {
const error = await this.parseErrorResponse(response);
throw error;
}
return response;
});
}
// DMARC record checking
async checkDMARC(domain, options = {}) {
try {
const response = await this.client.getCached(
'/api/dmarc-check',
{ domain, ...options },
60000 // Cache for 1 minute only (DNS changes)
);
const data = await response.json();
return {
domain,
dmarc_record: data.dmarc_record,
policy: data.policy,
subdomain_policy: data.subdomain_policy,
alignment: data.alignment,
valid: data.valid,
warnings: data.warnings || [],
recommendations: data.recommendations || []
};
} catch (error) {
const handled = await this.errorHandler.handleError(error, {
operation: 'checkDMARC',
domain
});
throw handled.error;
}
}
// SPF record validation
async validateSPF(domain, ip = null) {
try {
const params = { domain };
if (ip) params.ip = ip;
const response = await this.client.getCached(
'/api/spf-check',
params,
60000
);
const data = await response.json();
return {
domain,
spf_record: data.spf_record,
valid: data.valid,
dns_lookups: data.dns_lookups,
mechanisms: data.mechanisms,
warnings: data.warnings || [],
ip_authorized: data.ip_authorized,
suggestions: data.suggestions || []
};
} catch (error) {
const handled = await this.errorHandler.handleError(error, {
operation: 'validateSPF',
domain,
ip
});
throw handled.error;
}
}
// SMTP server testing
async testSMTP(host, port = 25, options = {}) {
try {
const response = await this.client.get('/api/smtp-test', {
host,
port,
tls: options.tls || false,
auth: options.auth || false,
timeout: options.timeout || 10000
});
const data = await response.json();
return {
host,
port,
status: data.status,
response_time: data.response_time,
tls_support: data.tls_support,
auth_methods: data.auth_methods || [],
capabilities: data.capabilities || [],
error: data.error,
certificate_info: data.certificate_info
};
} catch (error) {
const handled = await this.errorHandler.handleError(error, {
operation: 'testSMTP',
host,
port
});
throw handled.error;
}
}
// Batch email validation
async validateEmailsBatch(emails) {
if (emails.length === 0) return [];
try {
// Use batch endpoint for efficiency
const response = await this.client.post('/api/email-validation/batch', {
emails,
checks: ['syntax', 'domain', 'mx', 'disposable']
});
const data = await response.json();
return data.results || [];
} catch (error) {
// Fallback to individual validation if batch fails
if (error.status === 404) {
return this.validateEmailsIndividually(emails);
}
const handled = await this.errorHandler.handleError(error, {
operation: 'validateEmailsBatch',
count: emails.length
});
throw handled.error;
}
}
// Individual validation fallback
async validateEmailsIndividually(emails) {
const requests = emails.map(email => ({
endpoint: '/api/email-validation',
options: {
method: 'GET',
params: { email }
}
}));
return this.client.batch(requests);
}
// Parse error responses
async parseErrorResponse(response) {
let errorData = { message: 'Unknown error' };
try {
errorData = await response.json();
} catch {
errorData.message = `HTTP ${response.status}: ${response.statusText}`;
}
return new APIError(
errorData.message,
response.status,
errorData.code || `HTTP_${response.status}`,
{
url: response.url,
details: errorData.details,
timestamp: new Date().toISOString()
}
);
}
// Health check
async healthCheck() {
try {
const response = await this.client.get('/api/health');
return await response.json();
} catch (error) {
return { status: 'error', error: error.message };
}
}
}Usage Examples and Patterns
// Using the email testing API in applications
// Initialize client
const emailAPI = new EmailTestingAPI('your-api-key', {
timeout: 15000,
retryAttempts: 2
});
// Example 1: Domain email configuration check
async function checkDomainEmailConfig(domain) {
try {
const [dmarc, spf, mx] = await Promise.all([
emailAPI.checkDMARC(domain),
emailAPI.validateSPF(domain),
emailAPI.checkMX(domain)
]);
return {
domain,
dmarc: {
configured: dmarc.valid,
policy: dmarc.policy,
issues: dmarc.warnings
},
spf: {
configured: spf.valid,
lookups: spf.dns_lookups,
issues: spf.warnings
},
mx: {
records: mx.mx_records,
accessible: mx.mx_records.every(r => r.status === 'accessible')
},
overall_score: calculateEmailScore(dmarc, spf, mx)
};
} catch (error) {
console.error('Domain check failed:', error.message);
return {
domain,
error: error.message,
retryable: error.retryable
};
}
}
// Example 2: Email server testing workflow
async function testEmailServer(config) {
const results = [];
// Test common ports
const ports = [25, 465, 587];
for (const port of ports) {
try {
const result = await emailAPI.testSMTP(config.host, port, {
tls: port !== 25,
auth: true,
timeout: 10000
});
results.push({
port,
status: result.status,
tls: result.tls_support,
auth_methods: result.auth_methods,
response_time: result.response_time,
recommended: port === 587 && result.tls_support
});
} catch (error) {
results.push({
port,
status: 'failed',
error: error.message
});
}
}
return {
host: config.host,
results,
recommended_port: results.find(r => r.recommended)?.port || 587,
all_tests_passed: results.every(r => r.status === 'connected')
};
}
// Example 3: Bulk email validation with progress
async function validateEmailList(emails, onProgress = null) {
const batchSize = 100;
const results = [];
for (let i = 0; i < emails.length; i += batchSize) {
const batch = emails.slice(i, i + batchSize);
try {
const batchResults = await emailAPI.validateEmailsBatch(batch);
results.push(...batchResults);
if (onProgress) {
onProgress({
processed: results.length,
total: emails.length,
percentage: Math.round((results.length / emails.length) * 100)
});
}
} catch (error) {
console.error(`Batch ${i / batchSize + 1} failed:`, error.message);
// Add error entries for failed batch
batch.forEach(email => {
results.push({
email,
valid: false,
error: error.message
});
});
}
// Small delay between batches to be respectful
await new Promise(resolve => setTimeout(resolve, 100));
}
return {
total: emails.length,
valid: results.filter(r => r.valid).length,
invalid: results.filter(r => !r.valid && !r.error).length,
errors: results.filter(r => r.error).length,
results
};
}
// Example 4: Monitoring and alerting
class EmailMonitor {
constructor(domains, apiKey) {
this.domains = domains;
this.api = new EmailTestingAPI(apiKey);
this.lastResults = new Map();
}
async monitor() {
const results = new Map();
for (const domain of this.domains) {
try {
const [dmarc, spf] = await Promise.all([
this.api.checkDMARC(domain),
this.api.validateSPF(domain)
]);
const current = { dmarc, spf, timestamp: Date.now() };
const previous = this.lastResults.get(domain);
results.set(domain, current);
// Check for changes
if (previous && this.hasSignificantChanges(previous, current)) {
await this.sendAlert(domain, previous, current);
}
} catch (error) {
console.error(`Monitoring failed for ${domain}:`, error.message);
await this.sendErrorAlert(domain, error);
}
}
this.lastResults = results;
return results;
}
hasSignificantChanges(previous, current) {
// Check if DMARC policy changed
if (previous.dmarc.policy !== current.dmarc.policy) {
return true;
}
// Check if SPF record changed
if (previous.spf.spf_record !== current.spf.spf_record) {
return true;
}
// Check if validation status changed
if (previous.dmarc.valid !== current.dmarc.valid ||
previous.spf.valid !== current.spf.valid) {
return true;
}
return false;
}
async sendAlert(domain, previous, current) {
const changes = this.identifyChanges(previous, current);
console.log(`ALERT: Email configuration changed for ${domain}:`, changes);
// Send to monitoring system, Slack, email, etc.
}
async sendErrorAlert(domain, error) {
console.log(`ERROR: Monitoring failed for ${domain}:`, error.message);
}
}API integration success depends on proper error handling, rate limiting awareness, and defensive programming practices.
Testing and Debugging
API Testing Strategies
// Comprehensive API testing approach
// Mock API responses for testing
class MockAPIClient {
constructor(responses = {}) {
this.responses = responses;
this.requests = [];
}
async request(endpoint, options = {}) {
this.requests.push({ endpoint, options, timestamp: Date.now() });
const key = `${options.method || 'GET'} ${endpoint}`;
const response = this.responses[key];
if (!response) {
throw new APIError(`No mock response for ${key}`, 404, 'NOT_FOUND');
}
// Simulate delay
if (response.delay) {
await new Promise(resolve => setTimeout(resolve, response.delay));
}
// Simulate error
if (response.error) {
throw new APIError(response.error.message, response.error.status, response.error.code);
}
return {
ok: response.status >= 200 && response.status < 300,
status: response.status || 200,
headers: new Map(Object.entries(response.headers || {})),
json: () => Promise.resolve(response.data)
};
}
getRequests() {
return this.requests;
}
clearRequests() {
this.requests = [];
}
}
// Test suite for API integration
describe('EmailTestingAPI', () => {
let api;
let mockClient;
beforeEach(() => {
mockClient = new MockAPIClient({
'GET /api/dmarc-check': {
status: 200,
data: {
domain: 'example.com',
dmarc_record: 'v=DMARC1; p=reject; rua=mailto:[email protected]',
policy: 'reject',
valid: true
}
},
'GET /api/spf-check': {
status: 200,
data: {
domain: 'example.com',
spf_record: 'v=spf1 include:_spf.google.com ~all',
valid: true,
dns_lookups: 3
}
}
});
api = new EmailTestingAPI('test-key');
api.client = mockClient;
});
describe('checkDMARC', () => {
it('should return DMARC information for valid domain', async () => {
const result = await api.checkDMARC('example.com');
expect(result.domain).toBe('example.com');
expect(result.valid).toBe(true);
expect(result.policy).toBe('reject');
expect(result.dmarc_record).toContain('v=DMARC1');
});
it('should handle API errors gracefully', async () => {
mockClient.responses['GET /api/dmarc-check'] = {
error: { message: 'Domain not found', status: 404, code: 'DOMAIN_NOT_FOUND' }
};
await expect(api.checkDMARC('invalid.com')).rejects.toThrow('Domain not found');
});
it('should cache results appropriately', async () => {
await api.checkDMARC('example.com');
await api.checkDMARC('example.com'); // Should use cache
const requests = mockClient.getRequests();
expect(requests).toHaveLength(1); // Only one actual request
});
});
describe('batch operations', () => {
it('should handle batch validation efficiently', async () => {
const emails = ['[email protected]', '[email protected]', '[email protected]'];
mockClient.responses['POST /api/email-validation/batch'] = {
status: 200,
data: {
results: emails.map(email => ({ email, valid: true, reason: 'valid' }))
}
};
const results = await api.validateEmailsBatch(emails);
expect(results).toHaveLength(3);
expect(results.every(r => r.valid)).toBe(true);
});
});
describe('error handling', () => {
it('should retry on retryable errors', async () => {
let callCount = 0;
const originalRequest = mockClient.request.bind(mockClient);
mockClient.request = async (...args) => {
callCount++;
if (callCount < 3) {
throw new APIError('Service unavailable', 503, 'SERVICE_UNAVAILABLE');
}
return originalRequest(...args);
};
const result = await api.checkDMARC('example.com');
expect(result.valid).toBe(true);
expect(callCount).toBe(3); // Initial call + 2 retries
});
});
});Conclusion: API Integration Excellence
Successful API integration requires attention to authentication, error handling, performance, and user experience. The patterns and examples in this guide provide a solid foundation for building reliable, scalable applications that depend on external APIs.
- Design robust error handling from the start
- Implement proper authentication and security practices
- Use caching and batching to optimize performance
- Plan for rate limiting and service availability
- Test thoroughly with mocks and real integration scenarios
- Monitor API usage and performance in production