Skip to content

Best Practices

Learn how to build robust, secure, and efficient messaging applications with MsGine.

Security

Protect Your API Token

Never expose your API token in client-side code or public repositories:

typescript
// ❌ Bad - Never hardcode tokens
const client = new MsGineClient({
  apiToken: 'msgine_live_abc123...'
})

// ✅ Good - Use environment variables
const client = new MsGineClient({
  apiToken: process.env.MSGINE_API_TOKEN!
})

Use HTTPS Only

Always use HTTPS for webhook URLs and API requests:

typescript
// ❌ Bad
callbackUrl: 'http://myapp.com/webhook'

// ✅ Good
callbackUrl: 'https://myapp.com/webhook'

Validate Webhook Signatures

Always verify webhook signatures to prevent spoofing:

typescript
import crypto from 'crypto'

function verifyWebhookSignature(signature: string, payload: string): boolean {
  const secret = process.env.MSGINE_WEBHOOK_SECRET!
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

Sanitize User Input

Always validate and sanitize phone numbers and message content:

typescript
function sanitizePhoneNumber(phone: string): string {
  // Remove all non-digit characters except +
  const cleaned = phone.replace(/[^\d+]/g, '')

  // Ensure E.164 format
  if (!cleaned.startsWith('+')) {
    throw new Error('Phone number must include country code with + prefix')
  }

  return cleaned
}

function sanitizeMessage(message: string): string {
  // Remove potentially harmful content
  const sanitized = message
    .trim()
    .replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters

  if (sanitized.length === 0) {
    throw new Error('Message cannot be empty')
  }

  if (sanitized.length > 1600) {
    throw new Error('Message exceeds maximum length')
  }

  return sanitized
}

Error Handling

Always Use Try-Catch

Wrap all API calls in try-catch blocks:

typescript
async function sendMessage(to: string, message: string) {
  try {
    const result = await client.sendSms({ to, message })
    return result
  } catch (error) {
    if (error instanceof MsGineError) {
      console.error('MsGine API Error:', error.code, error.message)
      // Handle specific error codes
    } else {
      console.error('Unexpected error:', error)
    }
    throw error
  }
}

Handle Specific Error Codes

Implement specific handling for different error types:

typescript
try {
  await client.sendSms({ to, message })
} catch (error) {
  if (error instanceof MsGineError) {
    switch (error.code) {
      case 'invalid_phone_number':
        notifyUser('Invalid phone number format')
        break

      case 'insufficient_balance':
        notifyAdmin('Account balance low')
        await topUpBalance()
        break

      case 'rate_limit_exceeded':
        await queueForLater(to, message)
        break

      default:
        logToMonitoring(error)
    }
  }
}

Implement Graceful Degradation

Provide fallback options when SMS delivery fails:

typescript
async function sendNotification(user: User, message: string) {
  try {
    // Try SMS first
    return await client.sendSms({
      to: user.phone,
      message
    })
  } catch (error) {
    console.error('SMS failed, trying email fallback')

    // Fallback to email
    return await sendEmail({
      to: user.email,
      subject: 'Notification',
      body: message
    })
  }
}

Performance Optimization

Use Batch Operations

Send multiple messages in a single request:

typescript
// ❌ Not optimal - Multiple requests
for (const user of users) {
  await client.sendSms({
    to: user.phone,
    message: 'Hello!'
  })
}

// ✅ Better - Single batch request
await client.sendSms({
  to: users.map(u => u.phone),
  message: 'Hello!'
})

Implement Request Queuing

For high-volume applications, implement a message queue:

typescript
import Bull from 'bull'

const messageQueue = new Bull('messages', {
  redis: process.env.REDIS_URL
})

// Producer: Add messages to queue
async function queueMessage(to: string, message: string) {
  await messageQueue.add({
    to,
    message
  }, {
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 2000
    }
  })
}

// Consumer: Process messages from queue
messageQueue.process(async (job) => {
  const { to, message } = job.data
  return await client.sendSms({ to, message })
})

Cache Account Information

Cache account balance and info to reduce API calls:

typescript
import { CacheService } from './cache'

const cache = new CacheService()

async function getAccountBalance(): Promise<number> {
  const cached = await cache.get('account:balance')
  if (cached) {
    return cached
  }

  const account = await client.getAccount()
  await cache.set('account:balance', account.balance, 300) // 5 min TTL

  return account.balance
}

Use Webhooks Instead of Polling

Use webhooks for delivery status instead of polling:

typescript
// ❌ Bad - Polling
async function waitForDelivery(messageId: string) {
  while (true) {
    const status = await client.getMessageStatus(messageId)
    if (status.status === 'delivered') {
      return status
    }
    await sleep(5000) // Poll every 5 seconds
  }
}

// ✅ Good - Use webhooks
await client.sendSms({
  to: phone,
  message: 'Hello!',
  callbackUrl: 'https://myapp.com/webhook'
})

// Handle webhook to get delivery status
app.post('/webhook', (req, res) => {
  const { id, status } = req.body
  if (status === 'delivered') {
    // Message delivered
  }
  res.status(200).send()
})

Message Content

Keep Messages Concise

Shorter messages are more likely to be read and cost less:

typescript
// ❌ Too verbose
const message = 'Hello! This is a notification to inform you that your order #12345 has been successfully processed and is now ready for shipment.'

// ✅ Concise
const message = 'Order #12345 processed and ready for shipment!'

Include Clear Call-to-Action

Make it clear what action the recipient should take:

typescript
// ❌ Vague
const message = 'Your verification code is 123456'

// ✅ Clear CTA
const message = 'Your verification code is 123456. Enter it in the app to continue. Expires in 10 minutes.'

Personalize When Possible

Use recipient names and relevant details:

typescript
// ❌ Generic
const message = 'Your order is ready'

// ✅ Personalized
const message = `Hi ${user.firstName}, your order #${order.id} is ready for pickup!`

Avoid Spam Triggers

Don't use all caps, excessive punctuation, or spam keywords:

typescript
// ❌ Spam-like
const message = 'FREE!!! CLICK NOW!!! LIMITED TIME OFFER!!!'

// ✅ Professional
const message = 'Limited time offer: 20% off your next purchase. Use code SAVE20.'

Rate Limiting

Implement Exponential Backoff

Retry failed requests with increasing delays:

typescript
async function sendWithBackoff(to: string, message: string, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.sendSms({ to, message })
    } catch (error) {
      if (error instanceof MsGineError && error.code === 'rate_limit_exceeded') {
        const delay = Math.pow(2, attempt) * 1000
        console.log(`Rate limited, retrying in ${delay}ms`)
        await sleep(delay)
      } else {
        throw error
      }
    }
  }
  throw new Error('Max retries exceeded')
}

Monitor Rate Limit Usage

Track your rate limit usage to avoid hitting limits:

typescript
const client = new MsGineClient({
  apiToken: process.env.MSGINE_API_TOKEN!,
  onError: (error) => {
    if (error.code === 'rate_limit_exceeded') {
      // Alert monitoring service
      monitoring.alert('Rate limit exceeded', {
        retryAfter: error.details.retryAfter
      })
    }
  }
})

Testing

Use Test Mode

Use test API tokens for development and testing:

typescript
const client = new MsGineClient({
  apiToken: process.env.NODE_ENV === 'production'
    ? process.env.MSGINE_LIVE_TOKEN!
    : process.env.MSGINE_TEST_TOKEN!
})

Validate Before Sending

Validate inputs before making API calls:

typescript
function validateSmsRequest(to: string, message: string): void {
  // Validate phone number format
  if (!/^\+\d{10,15}$/.test(to)) {
    throw new Error('Invalid phone number format')
  }

  // Validate message length
  if (message.length === 0 || message.length > 1600) {
    throw new Error('Invalid message length')
  }

  // Check for required content
  if (message.trim().length === 0) {
    throw new Error('Message cannot be empty')
  }
}

async function sendSms(to: string, message: string) {
  validateSmsRequest(to, message)
  return await client.sendSms({ to, message })
}

Mock API Calls in Tests

Mock the SDK in unit tests:

typescript
import { jest } from '@jest/globals'
import { MsGineClient } from '@msgine/sdk'

jest.mock('@msgine/sdk')

describe('SMS Service', () => {
  it('should send SMS', async () => {
    const mockSendSms = jest.fn().mockResolvedValue({
      id: 'msg_123',
      status: 'pending'
    })

    ;(MsGineClient as jest.Mock).mockImplementation(() => ({
      sendSms: mockSendSms
    }))

    const service = new SmsService()
    await service.send('+256701521269', 'Hello')

    expect(mockSendSms).toHaveBeenCalledWith({
      to: '+256701521269',
      message: 'Hello'
    })
  })
})

Monitoring and Logging

Log All Messages

Keep audit logs of all sent messages:

typescript
async function sendSmsWithLogging(to: string, message: string) {
  const startTime = Date.now()

  try {
    const result = await client.sendSms({ to, message })

    logger.info('SMS sent successfully', {
      messageId: result.id,
      to,
      status: result.status,
      cost: result.cost,
      duration: Date.now() - startTime
    })

    return result
  } catch (error) {
    logger.error('SMS failed', {
      to,
      error: error.message,
      duration: Date.now() - startTime
    })

    throw error
  }
}

Track Delivery Rates

Monitor delivery success rates:

typescript
const metrics = {
  sent: 0,
  delivered: 0,
  failed: 0
}

app.post('/webhook', (req, res) => {
  const { status } = req.body

  switch (status) {
    case 'delivered':
      metrics.delivered++
      break
    case 'failed':
      metrics.failed++
      break
  }

  const deliveryRate = (metrics.delivered / metrics.sent) * 100
  console.log(`Delivery rate: ${deliveryRate}%`)

  res.status(200).send()
})

Set Up Alerts

Configure alerts for critical issues:

typescript
const client = new MsGineClient({
  apiToken: process.env.MSGINE_API_TOKEN!,
  onError: async (error) => {
    if (error.code === 'insufficient_balance') {
      await alertAdmin('Low balance', {
        balance: error.details.balance,
        currency: error.details.currency
      })
    }

    if (error.statusCode >= 500) {
      await alertDevOps('API server error', {
        statusCode: error.statusCode,
        message: error.message
      })
    }
  }
})

Cost Optimization

Check Balance Regularly

Monitor your account balance to avoid service interruption:

typescript
async function checkBalanceAndAlert() {
  const account = await client.getAccount()

  if (account.balance < 10) {
    await alertAdmin('Low account balance', {
      balance: account.balance,
      currency: account.currency
    })
  }
}

// Check balance every hour
setInterval(checkBalanceAndAlert, 3600000)

Estimate Costs Before Sending

Calculate message costs before sending:

typescript
function estimateSmsCost(message: string, recipients: number): number {
  const messageLength = message.length
  const partsPerMessage = Math.ceil(messageLength / 160)
  const costPerPart = 0.05 // USD
  const totalCost = partsPerMessage * recipients * costPerPart

  return totalCost
}

// Check budget before sending
async function sendWithBudgetCheck(to: string[], message: string) {
  const estimatedCost = estimateSmsCost(message, to.length)

  if (estimatedCost > budget.remaining) {
    throw new Error('Insufficient budget for this operation')
  }

  return await client.sendSms({ to, message })
}

Next Steps

Released under the MIT License.