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:
// ❌ 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:
// ❌ Bad
callbackUrl: 'http://myapp.com/webhook'
// ✅ Good
callbackUrl: 'https://myapp.com/webhook'Validate Webhook Signatures
Always verify webhook signatures to prevent spoofing:
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:
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:
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:
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:
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:
// ❌ 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:
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:
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:
// ❌ 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:
// ❌ 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:
// ❌ 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:
// ❌ 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:
// ❌ 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
- Security Guide - Advanced security practices
- Troubleshooting - Common issues and solutions
- Error Handling - Detailed error handling guide