Skip to main content

Common Solutions & Patterns

Proven patterns and code examples for handling common API challenges.

Retry Logic Patterns

Exponential Backoff

async function sendWithRetry(messageData, maxRetries = 3) {
let attempt = 0;

while (attempt < maxRetries) {
try {
const result = await sendMessage(messageData);
return result;
} catch (error) {
attempt++;

// Don't retry authentication errors
if (error.message.includes('Data is not valid') ||
error.message.includes('not authorized')) {
throw error;
}

// Exponential backoff for server errors
if (error.status >= 500 || error.message.includes('Service unavailable')) {
if (attempt < maxRetries) {
const backoffTime = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
await new Promise(resolve => setTimeout(resolve, backoffTime));
continue;
}
}

// Don't retry client errors (4xx)
throw error;
}
}

throw new Error(`Failed after ${maxRetries} attempts`);
}

Rate Limiting Handler

class RateLimitedClient {
constructor(requestsPerSecond = 10) {
this.requestsPerSecond = requestsPerSecond;
this.lastRequestTime = 0;
this.requestQueue = [];
this.processing = false;
}

async sendMessage(messageData) {
return new Promise((resolve, reject) => {
this.requestQueue.push({ messageData, resolve, reject });
this.processQueue();
});
}

async processQueue() {
if (this.processing || this.requestQueue.length === 0) {
return;
}

this.processing = true;

while (this.requestQueue.length > 0) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
const minInterval = 1000 / this.requestsPerSecond;

if (timeSinceLastRequest < minInterval) {
await new Promise(resolve =>
setTimeout(resolve, minInterval - timeSinceLastRequest)
);
}

const { messageData, resolve, reject } = this.requestQueue.shift();
this.lastRequestTime = Date.now();

try {
const result = await this.sendMessageDirect(messageData);
resolve(result);
} catch (error) {
reject(error);
}
}

this.processing = false;
}

async sendMessageDirect(messageData) {
// Direct API call implementation
const response = await fetch('https://m5api.groupcall.com/api/SendMessage_V3/SMS', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messageData: [messageData] })
});

return await response.json();
}
}

// Usage
const client = new RateLimitedClient(5); // 5 requests per second
const result = await client.sendMessage(messageData);

Response Validation

Comprehensive Response Handler

class ResponseValidator {
static validate(response, httpStatus) {
// Handle HTTP-level errors
if (httpStatus >= 500) {
throw new ApiError('Server error occurred', 'SERVER_ERROR', httpStatus);
}

if (httpStatus >= 400 && httpStatus < 500) {
throw new ApiError('Client error occurred', 'CLIENT_ERROR', httpStatus);
}

// Handle API response format
if (!Array.isArray(response)) {
throw new ApiError('Invalid response format', 'INVALID_FORMAT');
}

if (response.length === 0) {
throw new ApiError('Empty response received', 'EMPTY_RESPONSE');
}

const result = response[0];

// Handle null response
if (result === null) {
throw new ApiError('Null response received', 'NULL_RESPONSE');
}

// Check for success
if (result.errorMsg &&
result.errorMsg.startsWith('OK') &&
result.MessageId) {
return {
success: true,
messageId: result.MessageId,
status: result.statusMsg,
transmitDateTime: result.transmitDateTime,
warnings: result.WarningMessages || []
};
}

// Handle error conditions
const errorMsg = result.errorMsg ||
(result.WarningMessages && result.WarningMessages[0]) ||
'Unknown error';

// Categorize errors
if (errorMsg.includes('Data is not valid')) {
throw new ApiError(errorMsg, 'AUTH_ERROR', result);
}

if (errorMsg.includes('No recipients')) {
throw new ApiError(errorMsg, 'INVALID_RECIPIENTS', result);
}

if (errorMsg.includes('Rate limit') || errorMsg.includes('Too many')) {
throw new ApiError(errorMsg, 'RATE_LIMITED', result);
}

throw new ApiError(errorMsg, 'API_ERROR', result);
}
}

class ApiError extends Error {
constructor(message, code, data) {
super(message);
this.name = 'ApiError';
this.code = code;
this.data = data;
}
}

// Usage
try {
const response = await fetch(apiUrl, options);
const data = await response.json();
const result = ResponseValidator.validate(data, response.status);
console.log('Message sent:', result);
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error (${error.code}):`, error.message);
// Handle specific error types
switch (error.code) {
case 'AUTH_ERROR':
// Handle authentication issues
break;
case 'RATE_LIMITED':
// Implement backoff
break;
// ... other cases
}
} else {
console.error('Unexpected error:', error);
}
}

Input Validation

Phone Number Validation

class PhoneValidator {
static validateE164(phoneNumber) {
// E.164 format: +[country code][number]
const e164Regex = /^\+[1-9]\d{1,14}$/;
return e164Regex.test(phoneNumber);
}

static normalizePhoneNumber(phoneNumber, defaultCountryCode = '44') {
// Remove all non-digit characters except +
let cleaned = phoneNumber.replace(/[^\d+]/g, '');

// Handle different input formats
if (cleaned.startsWith('00')) {
// International format starting with 00
cleaned = '+' + cleaned.substring(2);
} else if (cleaned.startsWith('0') && !cleaned.startsWith('00')) {
// National format starting with 0
cleaned = '+' + defaultCountryCode + cleaned.substring(1);
} else if (!cleaned.startsWith('+')) {
// Assume national format without leading 0
cleaned = '+' + defaultCountryCode + cleaned;
}

return cleaned;
}

static validateAndNormalize(phoneNumbers, defaultCountryCode = '44') {
const results = {
valid: [],
invalid: []
};

phoneNumbers.forEach(number => {
try {
const normalized = this.normalizePhoneNumber(number, defaultCountryCode);
if (this.validateE164(normalized)) {
results.valid.push(normalized);
} else {
results.invalid.push({ original: number, reason: 'Invalid format' });
}
} catch (error) {
results.invalid.push({ original: number, reason: error.message });
}
});

return results;
}
}

// Usage
const phoneNumbers = ['07700 900123', '+44 7700 900123', '447700900123'];
const validation = PhoneValidator.validateAndNormalize(phoneNumbers);

console.log('Valid numbers:', validation.valid);
console.log('Invalid numbers:', validation.invalid);

Message Content Validation

class MessageValidator {
static validateSMS(message) {
const maxLength = 918; // 6 concatenated SMS parts
const errors = [];

if (!message || typeof message !== 'string') {
errors.push('Message must be a non-empty string');
return { valid: false, errors };
}

if (message.length === 0) {
errors.push('Message cannot be empty');
}

if (message.length > maxLength) {
errors.push(`Message exceeds maximum length of ${maxLength} characters`);
}

// Check for problematic characters
const problematicChars = message.match(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g);
if (problematicChars) {
errors.push('Message contains control characters that may cause issues');
}

// Count actual SMS parts
const singleSMSLength = 160;
const concatenatedSMSLength = 153; // Reduced due to concatenation headers

let parts;
if (message.length <= singleSMSLength) {
parts = 1;
} else {
parts = Math.ceil(message.length / concatenatedSMSLength);
}

return {
valid: errors.length === 0,
errors,
info: {
length: message.length,
parts,
cost: parts // Assuming 1 credit per part
}
};
}

static sanitizeMessage(message) {
// Remove control characters
let sanitized = message.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '');

// Normalize line endings
sanitized = sanitized.replace(/\r\n/g, '\n').replace(/\r/g, '\n');

// Trim whitespace
sanitized = sanitized.trim();

return sanitized;
}
}

// Usage
const validation = MessageValidator.validateSMS(userMessage);
if (!validation.valid) {
console.error('Message validation failed:', validation.errors);
} else {
console.log('Message validated:', validation.info);
}

Bulk Messaging Patterns

Batch Processing

class BulkMessageSender {
constructor(client, options = {}) {
this.client = client;
this.batchSize = options.batchSize || 50;
this.concurrency = options.concurrency || 3;
this.retryAttempts = options.retryAttempts || 3;
}

async sendBulkMessages(recipients, message, options = {}) {
const results = {
successful: [],
failed: [],
summary: {
total: recipients.length,
sent: 0,
failed: 0
}
};

// Validate recipients first
const validation = PhoneValidator.validateAndNormalize(recipients);

if (validation.invalid.length > 0) {
console.warn(`${validation.invalid.length} invalid recipients will be skipped`);
results.failed.push(...validation.invalid.map(invalid => ({
recipient: invalid.original,
error: invalid.reason
})));
}

// Process in batches
const batches = this.createBatches(validation.valid, this.batchSize);
const promises = [];

for (let i = 0; i < batches.length; i += this.concurrency) {
const concurrentBatches = batches.slice(i, i + this.concurrency);

const batchPromises = concurrentBatches.map(batch =>
this.processBatch(batch, message, options)
.then(batchResults => {
results.successful.push(...batchResults.successful);
results.failed.push(...batchResults.failed);
})
.catch(error => {
// Handle batch-level failures
batch.forEach(recipient => {
results.failed.push({
recipient,
error: error.message
});
});
})
);

await Promise.all(batchPromises);

// Optional delay between concurrent batch groups
if (i + this.concurrency < batches.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}

results.summary.sent = results.successful.length;
results.summary.failed = results.failed.length;

return results;
}

createBatches(items, batchSize) {
const batches = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}

async processBatch(recipients, message, options) {
const batchResults = {
successful: [],
failed: []
};

const messageData = {
CustomerId: this.client.customerId,
Password: this.client.apiKey,
recipients,
message,
...options
};

try {
const response = await this.client.sendMessage(messageData);

// Process response for batch
recipients.forEach((recipient, index) => {
batchResults.successful.push({
recipient,
messageId: response.messageId || `batch_${Date.now()}_${index}`,
status: 'queued'
});
});

} catch (error) {
recipients.forEach(recipient => {
batchResults.failed.push({
recipient,
error: error.message
});
});
}

return batchResults;
}
}

// Usage
const bulkSender = new BulkMessageSender(client, {
batchSize: 50,
concurrency: 3
});

const recipients = ['+447700900123', '+447700900124', /* ... */];
const message = 'Important school announcement';

const results = await bulkSender.sendBulkMessages(recipients, message);
console.log(`Sent: ${results.summary.sent}, Failed: ${results.summary.failed}`);

Error Recovery Strategies

Circuit Breaker Pattern

class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.timeout = options.timeout || 60000; // 1 minute
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.lastFailureTime = null;
}

async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}

try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}

onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}

onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();

if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
}
}

// Usage
const circuitBreaker = new CircuitBreaker({
failureThreshold: 3,
timeout: 30000
});

async function sendMessageWithCircuitBreaker(messageData) {
return circuitBreaker.execute(async () => {
return await client.sendMessage(messageData);
});
}

Monitoring and Logging

Comprehensive Logging

class MessageLogger {
constructor(options = {}) {
this.logLevel = options.logLevel || 'info';
this.includePayload = options.includePayload || false;
}

logRequest(messageData, metadata = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
type: 'request',
messageType: 'SMS',
recipientCount: messageData.recipients?.length || 0,
customerId: messageData.CustomerId,
metadata,
...(this.includePayload && { payload: this.sanitizePayload(messageData) })
};

console.log('MESSAGE_REQUEST:', JSON.stringify(logEntry));
}

logResponse(response, duration, messageData) {
const logEntry = {
timestamp: new Date().toISOString(),
type: 'response',
duration,
success: response.success,
messageId: response.messageId,
customerId: messageData.CustomerId,
recipientCount: messageData.recipients?.length || 0
};

console.log('MESSAGE_RESPONSE:', JSON.stringify(logEntry));
}

logError(error, messageData, metadata = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
type: 'error',
error: {
message: error.message,
code: error.code,
stack: error.stack
},
customerId: messageData.CustomerId,
recipientCount: messageData.recipients?.length || 0,
metadata
};

console.error('MESSAGE_ERROR:', JSON.stringify(logEntry));
}

sanitizePayload(payload) {
const sanitized = { ...payload };
if (sanitized.Password) {
sanitized.Password = '***REDACTED***';
}
return sanitized;
}
}

// Usage
const logger = new MessageLogger({ includePayload: false });

const startTime = Date.now();
logger.logRequest(messageData, { source: 'bulk_send' });

try {
const response = await client.sendMessage(messageData);
logger.logResponse(response, Date.now() - startTime, messageData);
} catch (error) {
logger.logError(error, messageData, { attemptNumber: 1 });
}

Related Documentation: