Send SMS with Node.js
Learn how to integrate Lineserve's SMS API into your Node.js applications for sending messages programmatically.
Prerequisitesโ
- Node.js 14+ installed
- Lineserve account with API key
- Basic knowledge of JavaScript and Node.js
Installationโ
Install the Lineserve SDKโ
npm install @lineserve/node
Alternative: Use HTTP Clientโ
If you prefer using a standard HTTP client:
npm install axios
# or
npm install node-fetch
Quick Startโ
Using the Lineserve SDKโ
const { LineserveClient } = require('@lineserve/node');
// Initialize the client
const client = new LineserveClient({
apiKey: 'your-api-key-here'
});
// Send a simple SMS
async function sendSMS() {
try {
const result = await client.sms.send({
to: '+254700123456',
message: 'Hello from Lineserve!',
from: 'YourBrand'
});
console.log('Message sent successfully:', result);
} catch (error) {
console.error('Error sending message:', error);
}
}
sendSMS();
Using Axiosโ
const axios = require('axios');
const API_KEY = 'your-api-key-here';
const BASE_URL = 'https://api.lineserve.com/v1';
async function sendSMS() {
try {
const response = await axios.post(`${BASE_URL}/sms/send`, {
to: '+254700123456',
message: 'Hello from Lineserve!',
from: 'YourBrand'
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
console.log('Message sent:', response.data);
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
sendSMS();
Environment Setupโ
Environment Variablesโ
Create a .env
file to store your API key securely:
# .env
LINESERVE_API_KEY=your-api-key-here
LINESERVE_SENDER_ID=YourBrand
Install dotenv to load environment variables:
npm install dotenv
Configuration Fileโ
// config.js
require('dotenv').config();
module.exports = {
lineserve: {
apiKey: process.env.LINESERVE_API_KEY,
senderId: process.env.LINESERVE_SENDER_ID,
baseUrl: 'https://api.lineserve.com/v1'
}
};
Complete SMS Serviceโ
SMS Service Classโ
// smsService.js
const { LineserveClient } = require('@lineserve/node');
const config = require('./config');
class SMSService {
constructor() {
this.client = new LineserveClient({
apiKey: config.lineserve.apiKey
});
}
async sendSingle(to, message, from = config.lineserve.senderId) {
try {
const result = await this.client.sms.send({
to,
message,
from
});
return {
success: true,
messageId: result.message_id,
status: result.status
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async sendBulk(recipients, message, from = config.lineserve.senderId) {
try {
const result = await this.client.sms.sendBulk({
recipients,
message,
from
});
return {
success: true,
campaignId: result.campaign_id,
totalMessages: result.total_messages
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async getStatus(messageId) {
try {
const result = await this.client.sms.getStatus(messageId);
return result;
} catch (error) {
throw new Error(`Failed to get message status: ${error.message}`);
}
}
async getBalance() {
try {
const result = await this.client.account.getBalance();
return result;
} catch (error) {
throw new Error(`Failed to get balance: ${error.message}`);
}
}
}
module.exports = SMSService;
Express.js Integrationโ
Basic Express Appโ
// app.js
const express = require('express');
const SMSService = require('./smsService');
const app = express();
const smsService = new SMSService();
app.use(express.json());
// Send single SMS
app.post('/send-sms', async (req, res) => {
const { to, message, from } = req.body;
if (!to || !message) {
return res.status(400).json({
error: 'Phone number and message are required'
});
}
const result = await smsService.sendSingle(to, message, from);
if (result.success) {
res.json({
message: 'SMS sent successfully',
messageId: result.messageId
});
} else {
res.status(500).json({
error: result.error
});
}
});
// Send bulk SMS
app.post('/send-bulk-sms', async (req, res) => {
const { recipients, message, from } = req.body;
if (!recipients || !Array.isArray(recipients) || !message) {
return res.status(400).json({
error: 'Recipients array and message are required'
});
}
const result = await smsService.sendBulk(recipients, message, from);
if (result.success) {
res.json({
message: 'Bulk SMS sent successfully',
campaignId: result.campaignId,
totalMessages: result.totalMessages
});
} else {
res.status(500).json({
error: result.error
});
}
});
// Check message status
app.get('/sms-status/:messageId', async (req, res) => {
try {
const status = await smsService.getStatus(req.params.messageId);
res.json(status);
} catch (error) {
res.status(500).json({
error: error.message
});
}
});
// Get account balance
app.get('/balance', async (req, res) => {
try {
const balance = await smsService.getBalance();
res.json(balance);
} catch (error) {
res.status(500).json({
error: error.message
});
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Advanced Featuresโ
Message Templatesโ
// messageTemplates.js
class MessageTemplates {
static welcome(name) {
return `Welcome to our service, ${name}! Your account has been created successfully.`;
}
static orderConfirmation(orderNumber, amount) {
return `Order #${orderNumber} confirmed. Total: $${amount}. Thank you for your purchase!`;
}
static otp(code) {
return `Your verification code is: ${code}. This code expires in 10 minutes.`;
}
static reminder(eventName, date) {
return `Reminder: ${eventName} is scheduled for ${date}. Don't forget!`;
}
static promotional(discount, code) {
return `Special offer! Get ${discount}% off with code ${code}. Valid until end of month.`;
}
}
module.exports = MessageTemplates;
Phone Number Validationโ
// phoneValidator.js
class PhoneValidator {
static isValid(phoneNumber) {
// Basic international phone number validation
const phoneRegex = /^\+[1-9]\d{1,14}$/;
return phoneRegex.test(phoneNumber);
}
static formatNumber(phoneNumber) {
// Remove all non-digit characters except +
let cleaned = phoneNumber.replace(/[^\d+]/g, '');
// Add + if not present
if (!cleaned.startsWith('+')) {
cleaned = '+' + cleaned;
}
return cleaned;
}
static getCountryCode(phoneNumber) {
const formatted = this.formatNumber(phoneNumber);
// Extract country code (simplified)
if (formatted.startsWith('+254')) return 'KE'; // Kenya
if (formatted.startsWith('+27')) return 'ZA'; // South Africa
if (formatted.startsWith('+234')) return 'NG'; // Nigeria
if (formatted.startsWith('+233')) return 'GH'; // Ghana
return 'UNKNOWN';
}
}
module.exports = PhoneValidator;
Retry Logicโ
// retryService.js
class RetryService {
static async withRetry(fn, maxRetries = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await this.sleep(delay);
delay *= 2; // Exponential backoff
}
}
}
static sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage example
async function sendSMSWithRetry(to, message) {
return await RetryService.withRetry(async () => {
return await smsService.sendSingle(to, message);
}, 3, 1000);
}
module.exports = RetryService;
Database Integrationโ
MongoDB Integrationโ
// models/Message.js
const mongoose = require('mongoose');
const messageSchema = new mongoose.Schema({
messageId: { type: String, required: true, unique: true },
to: { type: String, required: true },
message: { type: String, required: true },
from: { type: String, required: true },
status: { type: String, default: 'pending' },
sentAt: { type: Date, default: Date.now },
deliveredAt: { type: Date },
errorMessage: { type: String },
cost: { type: Number }
});
module.exports = mongoose.model('Message', messageSchema);
// Enhanced SMS Service with Database
const Message = require('./models/Message');
class EnhancedSMSService extends SMSService {
async sendAndLog(to, message, from) {
const result = await this.sendSingle(to, message, from);
if (result.success) {
// Save to database
const messageRecord = new Message({
messageId: result.messageId,
to,
message,
from,
status: result.status
});
await messageRecord.save();
}
return result;
}
async updateMessageStatus(messageId, status, deliveredAt = null) {
await Message.findOneAndUpdate(
{ messageId },
{
status,
deliveredAt: deliveredAt || (status === 'delivered' ? new Date() : null)
}
);
}
async getMessageHistory(phoneNumber, limit = 10) {
return await Message.find({ to: phoneNumber })
.sort({ sentAt: -1 })
.limit(limit);
}
}
Webhook Handlingโ
Delivery Status Webhooksโ
// webhookHandler.js
const express = require('express');
const crypto = require('crypto');
const Message = require('./models/Message');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhooks/sms-status', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-lineserve-signature'];
const webhookSecret = process.env.LINESERVE_WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.body, signature, webhookSecret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
if (event.type === 'sms.status_updated') {
const { message_id, status, delivered_at } = event.data;
await Message.findOneAndUpdate(
{ messageId: message_id },
{
status,
deliveredAt: delivered_at ? new Date(delivered_at) : null
}
);
console.log(`Message ${message_id} status updated to ${status}`);
}
res.status(200).send('OK');
});
Testingโ
Unit Tests with Jestโ
// tests/smsService.test.js
const SMSService = require('../smsService');
// Mock the Lineserve client
jest.mock('@lineserve/node');
describe('SMSService', () => {
let smsService;
beforeEach(() => {
smsService = new SMSService();
});
test('should send SMS successfully', async () => {
const mockSend = jest.fn().mockResolvedValue({
message_id: 'msg_123',
status: 'sent'
});
smsService.client.sms.send = mockSend;
const result = await smsService.sendSingle('+254700123456', 'Test message');
expect(result.success).toBe(true);
expect(result.messageId).toBe('msg_123');
expect(mockSend).toHaveBeenCalledWith({
to: '+254700123456',
message: 'Test message',
from: expect.any(String)
});
});
test('should handle SMS sending errors', async () => {
const mockSend = jest.fn().mockRejectedValue(new Error('API Error'));
smsService.client.sms.send = mockSend;
const result = await smsService.sendSingle('+254700123456', 'Test message');
expect(result.success).toBe(false);
expect(result.error).toBe('API Error');
});
});
Integration Testsโ
// tests/integration.test.js
const request = require('supertest');
const app = require('../app');
describe('SMS API Integration', () => {
test('POST /send-sms should send SMS', async () => {
const response = await request(app)
.post('/send-sms')
.send({
to: '+254700123456',
message: 'Test message'
});
expect(response.status).toBe(200);
expect(response.body.message).toBe('SMS sent successfully');
expect(response.body.messageId).toBeDefined();
});
test('POST /send-sms should validate required fields', async () => {
const response = await request(app)
.post('/send-sms')
.send({
message: 'Test message'
// Missing 'to' field
});
expect(response.status).toBe(400);
expect(response.body.error).toContain('required');
});
});
Error Handlingโ
Custom Error Classesโ
// errors/SMSError.js
class SMSError extends Error {
constructor(message, code, statusCode = 500) {
super(message);
this.name = 'SMSError';
this.code = code;
this.statusCode = statusCode;
}
}
class ValidationError extends SMSError {
constructor(message) {
super(message, 'VALIDATION_ERROR', 400);
this.name = 'ValidationError';
}
}
class APIError extends SMSError {
constructor(message, statusCode) {
super(message, 'API_ERROR', statusCode);
this.name = 'APIError';
}
}
module.exports = { SMSError, ValidationError, APIError };
Global Error Handlerโ
// middleware/errorHandler.js
const { SMSError } = require('../errors/SMSError');
function errorHandler(error, req, res, next) {
console.error('Error:', error);
if (error instanceof SMSError) {
return res.status(error.statusCode).json({
error: {
message: error.message,
code: error.code
}
});
}
// Default error response
res.status(500).json({
error: {
message: 'Internal server error',
code: 'INTERNAL_ERROR'
}
});
}
module.exports = errorHandler;
Production Considerationsโ
Rate Limitingโ
const rateLimit = require('express-rate-limit');
const smsRateLimit = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many SMS requests, please try again later'
});
app.use('/send-sms', smsRateLimit);
app.use('/send-bulk-sms', smsRateLimit);
Loggingโ
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Log SMS sending
logger.info('SMS sent', {
to: phoneNumber,
messageId: result.messageId,
timestamp: new Date().toISOString()
});
Health Checksโ
app.get('/health', async (req, res) => {
try {
// Check API connectivity
await smsService.getBalance();
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
services: {
api: 'connected',
database: 'connected'
}
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message
});
}
});