Best Practices
Learn how to build robust, secure, and efficient messaging applications with MsGine.
Security
Protect Your API Key
Never expose your API key in client-side code or public repositories:
// ❌ Bad - Never hardcode keys
const client = new MsGineClient({
apiKey: 'msgine_live_abc123...'
})
// ✅ Good - Use environment variables
const client = new MsGineClient({
apiKey: process.env.MSGINE_API_KEY!,
})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 control characters
const sanitized = message
.trim()
.replace(/[\x00-\x1F\x7F]/g, '')
if (sanitized.length === 0) {
throw new Error('Message cannot be empty')
}
if (sanitized.length > 1000) {
throw new Error('Message exceeds maximum length of 1000 characters')
}
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.sms.send({ 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.sms.send({ 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.sms.send({
to: user.phone,
message
})
} catch (error) {
console.error('SMS failed, trying email fallback')
// Fallback to email
return await client.email.send({
to: user.email,
subject: 'Notification',
body: message
})
}
}Performance Optimization
Use Batch Operations
Send to multiple recipients in a single request:
// ❌ Not optimal - Multiple requests
for (const user of users) {
await client.sms.send({
to: user.phone,
message: 'Hello!'
})
}
// ✅ Better - Single request for all recipients
await client.sms.send({
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.sms.send({ to, message })
})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 getMessageStatus(messageId)
if (status === 'delivered') {
return status
}
await sleep(5000)
}
}
// ✅ Good - Use webhooks
await client.sms.send({
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.sms.send({ 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')
}Testing
Use Test Mode
Use test API keys for development and testing:
const client = new MsGineClient({
apiKey: process.env.NODE_ENV === 'production'
? process.env.MSGINE_LIVE_KEY!
: process.env.MSGINE_TEST_KEY!,
})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 > 1000) {
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.sms.send({ 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 mockSend = jest.fn().mockResolvedValue({
id: 'msg_123',
status: 'pending'
})
;(MsGineClient as jest.Mock).mockImplementation(() => ({
sms: { send: mockSend }
}))
const service = new SmsService()
await service.send('+256701521269', 'Hello')
expect(mockSend).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.sms.send({ to, message })
logger.info('SMS sent successfully', {
messageId: result.id,
to,
status: result.status,
cost: result.cost,
currency: result.currency,
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()
})Cost Optimization
Estimate Costs Before Sending
Calculate message costs before sending (30 UGX per SMS in Uganda):
function estimateSmsCost(recipients: number): number {
const costPerSms = 30 // UGX
return recipients * costPerSms
}
async function sendWithBudgetCheck(to: string[], message: string, budgetUgx: number) {
const estimatedCost = estimateSmsCost(to.length)
if (estimatedCost > budgetUgx) {
throw new Error(`Estimated cost (${estimatedCost} UGX) exceeds budget (${budgetUgx} UGX)`)
}
return await client.sms.send({ to, message })
}Next Steps
- Security Guide - Advanced security practices
- Troubleshooting - Common issues and solutions
- Error Handling - Detailed error handling guide