This guide provides a comprehensive walkthrough for building a production-ready bulk SMS broadcasting system using Node.js, Express, and the Plivo communication platform. We'll cover everything from project setup and core implementation to error handling, security, deployment, and testing.
By the end of this guide, you will have a robust Express API capable of accepting a list of phone numbers and a message, then efficiently broadcasting that message to all recipients using Plivo's bulk messaging capabilities.
Author: Your Name / Organization Name Date: May 15, 2024
Project Overview and Goals
What We're Building:
We are building a Node.js application using the Express framework that exposes an API endpoint. This endpoint will receive requests containing a list of recipient phone numbers and a message body. The application will then leverage Plivo's API to send this message efficiently to all specified recipients in bulk.
Problems Solved:
- Inefficient Individual Sends: Sending SMS messages one by one via separate API calls is slow and resource-intensive for large lists.
- Scalability: Provides a foundation for sending messages to large audiences without overwhelming the system or hitting API rate limits improperly.
- Centralized Control: Offers a single API endpoint to manage broadcast operations.
Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building scalable, non-blocking, I/O-heavy applications like API servers.
- Express.js: A minimal and flexible Node.js web application framework providing routing, middleware, and other essential features.
- Plivo Node.js SDK: Simplifies interaction with the Plivo REST API for sending SMS.
- dotenv: A zero-dependency module that loads environment variables from a
.env
file intoprocess.env
. - (Optional but Recommended) A logging library (like
winston
orpino
) and a rate-limiting middleware (express-rate-limit
).
System Architecture:
+-------------+ +------------------------+ +----------------+ +-----------------+
| Client | ----> | Node.js/Express API | ----> | Plivo API | ----> | Recipient Phones|
| (e.g. curl, | | (POST /broadcast) | | (Bulk Message) | | (SMS Delivery) |
| WebApp) | | - Validate Request | +----------------+ +-----------------+
+-------------+ | - Authenticate |
| - Format Numbers (Batch)|
| - Call Plivo SDK |
+------------------------+
|
| (Logs/Errors)
V
+------------------------+
| Logging/Monitoring Svc |
+------------------------+
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. (Download Node.js)
- Plivo Account: A Plivo account with Auth ID, Auth Token, and at least one Plivo phone number (or Sender ID where applicable). (Sign up for Plivo)
- Trial Account Limitation: If using a trial account, you must add and verify the phone numbers you intend to send messages to in your Plivo console under ""Phone Numbers"" > ""Sandbox Numbers"". You cannot send to unverified numbers.
- Basic understanding of: JavaScript, Node.js, Express, REST APIs, and Promises/async-await.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
1. Create Project Directory:
Open your terminal and create a new directory for the project, then navigate into it.
mkdir plivo-bulk-sms-broadcaster
cd plivo-bulk-sms-broadcaster
2. Initialize Node.js Project:
This creates a package.json
file to manage dependencies and project metadata.
npm init -y
3. Install Dependencies:
We need Express for the web server, the Plivo SDK, and dotenv
for managing environment variables. We'll also add express-validator
for request validation and express-rate-limit
for basic security.
npm install express plivo-node dotenv express-validator express-rate-limit
4. Project Structure:
Create a basic structure for better organization.
mkdir src
mkdir src/routes
mkdir src/services
mkdir src/middleware
mkdir config
touch src/app.js
touch src/server.js
touch src/routes/broadcast.js
touch src/services/plivoService.js
touch src/middleware/auth.js
touch config/plivoConfig.js
touch .env
touch .gitignore
Your structure should look like this:
plivo-bulk-sms-broadcaster/
├── config/
│ └── plivoConfig.js
├── node_modules/
├── src/
│ ├── app.js # Express app configuration
│ ├── server.js # Server startup logic
│ ├── middleware/
│ │ └── auth.js # API Key authentication
│ ├── routes/
│ │ └── broadcast.js # Broadcast API route
│ └── services/
│ └── plivoService.js # Plivo interaction logic
├── .env # Environment variables (API keys, etc.) - DO NOT COMMIT
├── .gitignore # Files/folders to ignore in Git
├── package.json
└── package-lock.json
5. Configure .gitignore
:
Add node_modules
and .env
to your .gitignore
file to avoid committing dependencies and sensitive credentials.
# Dependencies
node_modules/
# Environment variables
.env
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
6. Setup Environment Variables (.env
):
Create a .env
file in the project root. This file will store sensitive information like API keys and configuration settings. Never commit this file to version control.
# Plivo Credentials
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
PLIVO_SENDER_ID=YOUR_PLIVO_PHONE_NUMBER_OR_SENDER_ID # e.g., +14155551212
# API Configuration
PORT=3000
API_KEY=YOUR_SECRET_API_KEY_FOR_THIS_SERVICE # Generate a strong, random key
# Plivo Settings (Optional overrides)
PLIVO_BULK_MESSAGE_LIMIT=1000 # Max recipients per Plivo API call
PLIVO_BATCH_DELAY_MS=250 # Delay between sending batches (to respect rate limits)
PLIVO_AUTH_ID
,PLIVO_AUTH_TOKEN
: Find these on your Plivo Console dashboard.PLIVO_SENDER_ID
: The Plivo phone number (in E.164 format, e.g.,+14155551212
) or approved Alphanumeric Sender ID you'll use to send messages. Remember restrictions apply (US/Canada require numbers).API_KEY
: A secret key you define. Clients calling your API will need to provide this key for authentication. Generate a strong, random string for this value.PLIVO_BULK_MESSAGE_LIMIT
: Plivo's documented limit per API call (currently 1000).PLIVO_BATCH_DELAY_MS
: A small delay to add between sending batches if you exceed the bulk limit, helping to stay within Plivo's overall rate limits (e.g., 5 messages/second default). Adjust as needed based on your Plivo account's limits.
7. Plivo Configuration (config/plivoConfig.js
):
Load Plivo credentials and settings securely using dotenv
.
// config/plivoConfig.js
require('dotenv').config(); // Load .env file variables
const plivoConfig = {
authId: process.env.PLIVO_AUTH_ID,
authToken: process.env.PLIVO_AUTH_TOKEN,
senderId: process.env.PLIVO_SENDER_ID,
bulkMessageLimit: parseInt(process.env.PLIVO_BULK_MESSAGE_LIMIT || '1000', 10),
batchDelayMs: parseInt(process.env.PLIVO_BATCH_DELAY_MS || '250', 10),
};
// Basic validation
if (!plivoConfig.authId || !plivoConfig.authToken || !plivoConfig.senderId) {
console.error(
'Error: Plivo Auth ID, Auth Token, or Sender ID not found in environment variables.'
);
console.error('Please check your .env file.');
process.exit(1); // Exit if essential config is missing
}
module.exports = plivoConfig;
- Why: This centralizes Plivo configuration, reads securely from environment variables, and performs basic validation on startup.
- Note: While placing
require('dotenv').config()
here works, a common practice is to load it only once at the very beginning of your application's entry point (e.g., top ofsrc/server.js
orsrc/app.js
) to ensure environment variables are available globally before any other modules are loaded. This approach avoids potential issues with load order and keeps configuration loading centralized.
2. Implementing Core Functionality (Plivo Service)
This service encapsulates the logic for interacting with the Plivo API, including formatting numbers for bulk sending and handling batching.
// src/services/plivoService.js
const plivo = require('plivo-node');
const plivoConfig = require('../../config/plivoConfig');
// Initialize Plivo client once
const client = new plivo.Client(plivoConfig.authId, plivoConfig.authToken);
/**
* Sends a message to multiple recipients using Plivo's bulk messaging.
* Handles batching if the recipient list exceeds Plivo's limit.
*
* @param {string[]} recipients - Array of recipient phone numbers in E.164 format.
* @param {string} message - The text message body.
* @returns {Promise<object[]>} - A promise that resolves with an array of Plivo API responses for each batch sent.
*/
async function sendBulkSms(recipients, message) {
if (!recipients || recipients.length === 0) {
throw new Error('Recipient list cannot be empty.');
}
if (!message) {
throw new Error('Message body cannot be empty.');
}
const results = [];
const batchSize = plivoConfig.bulkMessageLimit;
console.log(`Sending message to ${recipients.length} recipients in batches of ${batchSize}...`);
for (let i = 0; i < recipients.length; i += batchSize) {
const batch = recipients.slice(i_ i + batchSize);
const batchNumber = Math.floor(i / batchSize) + 1;
// Plivo requires '<' delimiter for bulk destination numbers
const destinationNumbers = batch.join('<');
const payload = {
src: plivoConfig.senderId_
dst: destinationNumbers_
text: message_
// Optional: Add URL for delivery reports
// url: 'YOUR_DELIVERY_REPORT_WEBHOOK_URL'_
// method: 'POST'
};
try {
console.log(`Sending batch ${batchNumber} to ${batch.length} recipients...`);
const response = await client.messages.create(payload);
console.log(`Batch ${batchNumber} sent successfully. Plivo Response:`_ response);
results.push(response);
// If there are more batches to send_ add a small delay
if (i + batchSize < recipients.length && plivoConfig.batchDelayMs > 0) {
console.log(`Waiting ${plivoConfig.batchDelayMs}ms before next batch...`);
await new Promise(resolve => setTimeout(resolve, plivoConfig.batchDelayMs));
}
} catch (error) {
console.error(`Error sending batch ${batchNumber}:`, error);
// Handle potential Plivo API errors within the loop.
// Production Consideration: The current implementation throws an error
// and stops processing immediately if any batch fails. For more robust
// bulk sending, consider implementing a partial failure strategy:
// 1. Collect errors for failed batches in a separate array.
// 2. Continue processing subsequent batches.
// 3. After the loop, if any errors occurred, either throw an aggregated
// error or return a result object indicating partial success and
// listing the specific failures.
// This prevents one bad batch from halting the entire broadcast.
throw new Error(`Failed to send SMS batch ${batchNumber}: ${error.message || error}`); // Current: Stop on first error
}
}
console.log('All batches processed.');
return results; // Array of Plivo responses for each successful batch
}
module.exports = {
sendBulkSms,
};
- Why:
- Encapsulates Plivo logic, making the API route cleaner.
- Uses
async/await
for clear asynchronous code. - Formats the
dst
parameter correctly usingjoin('<')
. - Implements batching using a
for
loop andslice
_ respecting thePLIVO_BULK_MESSAGE_LIMIT
. - Includes a configurable delay (
PLIVO_BATCH_DELAY_MS
) between batches to help manage rate limits. Plivo's default rate limit is often around 5 MPS_ so a small delay helps prevent hitting this limit when sending multiple batches quickly. - Provides basic logging for visibility.
- Includes basic input validation.
- Handles potential Plivo API errors within the loop.
3. Building the API Layer (Express Route)
Now_ let's create the Express route that will receive broadcast requests.
3.1. Authentication Middleware (src/middleware/auth.js
):
A simple API Key authentication middleware. In production_ consider more robust methods like JWT or OAuth2.
// src/middleware/auth.js
require('dotenv').config();
const API_KEY = process.env.API_KEY;
if (!API_KEY) {
console.warn('Warning: API_KEY environment variable not set. Authentication is disabled.');
}
function authenticateApiKey(req_ res_ next) {
// Skip authentication if API_KEY is not set (useful for local dev/testing)
if (!API_KEY) {
return next();
}
const providedApiKey = req.headers['x-api-key']; // Common practice uses 'x-api-key'_ but standard alternatives like 'Authorization: ApiKey YOUR_KEY' or 'Authorization: Bearer YOUR_KEY' are often preferred.
if (!providedApiKey) {
return res.status(401).json({ message: 'Unauthorized: API key missing.' });
}
if (providedApiKey !== API_KEY) {
return res.status(403).json({ message: 'Forbidden: Invalid API key.' });
}
next(); // API key is valid_ proceed to the next middleware/route handler
}
module.exports = authenticateApiKey;
- Why: Protects your API endpoint from unauthorized access using a simple shared secret passed in the header.
3.2. Broadcast Route (src/routes/broadcast.js
):
This file defines the /broadcast
endpoint_ validates the request_ and calls the plivoService
.
// src/routes/broadcast.js
const express = require('express');
const { body_ validationResult } = require('express-validator');
const plivoService = require('../services/plivoService');
const authenticateApiKey = require('../middleware/auth'); // Import auth middleware
const router = express.Router();
// E.164 regex pattern:
// ^\+ : Starts with a literal '+' sign.
// [1-9]: Followed by a digit from 1 to 9 (avoids +0).
// \d{1_14}: Followed by 1 to 14 digits (total length 3 to 16 chars including +).
// $: End of string.
// This is a common representation_ adjust if specific country code lengths need stricter validation.
const e164Pattern = /^\+[1-9]\d{1_14}$/;
// Apply API Key authentication to this route
router.use(authenticateApiKey);
router.post(
'/'_
[
// 1. Validate recipients: must be an array_ not empty
body('recipients')
.isArray({ min: 1 })
.withMessage('Recipients must be a non-empty array.')_
// 2. Validate each recipient in the array: must be a string matching E.164 format
body('recipients.*') // The '*' applies validation to each element in the array
.isString()
.matches(e164Pattern)
.withMessage('Each recipient must be a string in E.164 format (e.g._ +14155551212).')_
// 3. Validate message: must be a non-empty string
body('message')
.isString()
.notEmpty()
.withMessage('Message must be a non-empty string.')
.trim()_ // Remove leading/trailing whitespace
]_
async (req_ res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { recipients, message } = req.body;
try {
// Call the service to send the SMS
const results = await plivoService.sendBulkSms(recipients, message);
// Respond with success status and Plivo's response details
// 202 Accepted is suitable as the processing might take time,
// even though we await here. It indicates the request is valid
// and processing has begun (or completed).
res.status(202).json({
message: `Broadcast request accepted for ${recipients.length} recipients.`,
plivoResponses: results, // Contains response from each batch sent
});
} catch (error) {
console.error('Error in /broadcast route:', error);
// Determine appropriate status code based on error type
// If it's a Plivo API error passed up, it might indicate bad input
// or Plivo service issues. If it's an internal error, use 500.
res.status(500).json({
message: 'Failed to process broadcast request.',
error: error.message || 'Internal Server Error',
});
}
}
);
module.exports = router;
- Why:
- Uses
express.Router
for modular routing. - Integrates
authenticateApiKey
middleware. - Leverages
express-validator
for robust request validation:- Checks if
recipients
is a non-empty array. - Validates each element (
recipients.*
) in the array against an E.164 regex pattern. - Checks if
message
is a non-empty string.
- Checks if
- Returns detailed validation errors (400 Bad Request).
- Calls the
plivoService.sendBulkSms
function. - Provides informative success (202 Accepted) and error (500 Internal Server Error or potentially 4xx for specific Plivo errors) responses in JSON format.
- Uses
4. Integrating with Third-Party Services (Plivo Setup)
We already integrated the Plivo SDK in the service layer. This section focuses on obtaining and configuring the necessary credentials.
Steps to get Plivo Credentials:
- Log in to your Plivo Console: https://console.plivo.com/
- Find Auth ID and Auth Token: On the main dashboard after logging in, you'll see your
AUTH ID
andAUTH TOKEN
. - Copy Credentials: Carefully copy these values.
- Update
.env
: Paste theAUTH ID
intoPLIVO_AUTH_ID
and theAUTH TOKEN
intoPLIVO_AUTH_TOKEN
in your.env
file. - Obtain/Configure Sender ID:
- Navigate to ""Phone Numbers"" > ""Your Numbers"" in the Plivo console.
- If you don't have a number, go to ""Buy Numbers"" and purchase one suitable for SMS in your target regions (e.g., a US long code for sending to the US/Canada).
- If using an Alphanumeric Sender ID (where supported), ensure it's registered and approved via the ""Messaging"" > ""Sender IDs"" section.
- Update
.env
: Copy the chosen Plivo phone number (in E.164 format, e.g.,+14155551212
) or the approved Sender ID string intoPLIVO_SENDER_ID
in your.env
file. - Sandbox Numbers (Trial Accounts Only):
- If using a trial account, go to ""Phone Numbers"" > ""Sandbox Numbers"".
- Add and verify the phone numbers you intend to send messages to during testing. You cannot send to unverified numbers with a trial account.
Environment Variable Explanation:
PLIVO_AUTH_ID
(String): Your Plivo account identifier. Purpose: Authentication. Format: Alphanumeric string. Obtain: Plivo Console Dashboard.PLIVO_AUTH_TOKEN
(String): Your Plivo account secret token. Purpose: Authentication. Format: Alphanumeric string. Obtain: Plivo Console Dashboard.PLIVO_SENDER_ID
(String): The ""from"" number or ID for outgoing SMS. Purpose: Identify the sender to the recipient and comply with regulations. Format: E.164 phone number (+1...
) or approved Alphanumeric string. Obtain: Plivo Console Phone Numbers/Sender IDs section.API_KEY
(String): A secret key for securing your own API endpoint. Purpose: Basic authentication for your service. Format: Strong, random string. Obtain: You generate this yourself.PORT
(Number): Port number for the Express server. Purpose: Network configuration. Format: Integer (e.g., 3000). Obtain: Choose an available port.PLIVO_BULK_MESSAGE_LIMIT
(Number): Max recipients per Plivo API call. Purpose: Control batch size. Format: Integer. Obtain: Plivo documentation (default 1000).PLIVO_BATCH_DELAY_MS
(Number): Milliseconds to wait between sending batches. Purpose: Rate limiting. Format: Integer. Obtain: Based on your Plivo account limits and testing.
Security: Ensure your .env
file is never committed to Git and has restrictive file permissions on your server. Use secrets management tools (like AWS Secrets Manager, HashiCorp Vault, Doppler) in production environments.
5. Implementing Error Handling, Logging, and Retries
Robust error handling and logging are crucial for production systems.
Error Handling Strategy:
- Validation Errors: Handled by
express-validator
in the route, returning 400 Bad Request with details. - Authentication Errors: Handled by
auth.js
middleware, returning 401 Unauthorized or 403 Forbidden. - Plivo API Errors: Caught within
plivoService.js
. The Plivo SDK throws errors containing status codes and messages from the Plivo API. These are logged and currently re-thrown, causing the route handler to return a 500 Internal Server Error. Improvement: You could inspect the Plivo error status code and return more specific 4xx errors if appropriate (e.g., 400 for invalid number format if not caught by validation, 402 Payment Required for insufficient funds). - Network/Unexpected Errors: Caught by the
try...catch
blocks in the service and route, logged, and result in a 500 Internal Server Error.
Logging:
We've used basic console.log
and console.error
. For production, use a structured logging library:
npm install winston # Or pino, etc.
Example: Basic Winston Setup (e.g., in a new file config/logger.js
)
// config/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info', // Log level from env or default to info
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }), // Log stack traces
winston.format.json() // Log in JSON format
),
defaultMeta: { service: 'sms-broadcaster' }, // Optional: Add service name to logs
transports: [
// Default console transport
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(), // Add colors for readability in console
winston.format.simple()
)
})
// In production, add transports for files or log management services:
// new winston.transports.File({ filename: 'error.log', level: 'error' }),
// new winston.transports.File({ filename: 'combined.log' }),
],
});
// Example: Add file transport only if not in development
// if (process.env.NODE_ENV !== 'development') {
// logger.add(new winston.transports.File({ filename: 'combined.log' }));
// }
module.exports = logger;
// --- Usage Example (replace console.log/error in other files): ---
// const logger = require('../../config/logger'); // Adjust path as needed
// logger.info('Broadcast request received.', { recipientCount: recipients.length });
// logger.warn('API Key not set, authentication disabled.');
// logger.error('Failed to send SMS batch', { batchNumber: 1, error: error.message, stack: error.stack });
- Why: Structured logs (JSON) are easier to parse, filter, and analyze in log management systems. Libraries like Winston offer features like log levels, multiple transports (console, file, external services), and customizable formatting.
Retry Mechanisms:
The current implementation stops processing if a batch fails. For transient errors (network timeouts, temporary Plivo issues, rate limiting), implementing retries with exponential backoff is recommended.
(Conceptual Retry Logic in plivoService.js
)
async function sendSingleBatchWithRetry(payload, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
// logger.info(`Attempt ${attempt + 1} to send batch...`_ { dst: payload.dst }); // Use logger
return await client.messages.create(payload); // Attempt API call
} catch (error) {
attempt++;
// Check if the error is potentially retryable
const statusCode = error.statusCode || (error.response && error.response.status); // Plivo SDK might have statusCode or response.status
const isRetryable = (statusCode >= 500 || statusCode === 429 /* Too Many Requests */);
// logger.warn(`Batch send attempt ${attempt} failed.`, { error: error.message, statusCode, isRetryable }); // Use logger
if (isRetryable && attempt < maxRetries) {
const delay = Math.pow(2_ attempt) * 100; // Exponential backoff (100ms_ 200ms_ 400ms...)
// logger.warn(`Retrying in ${delay}ms...`); // Use logger
await new Promise(resolve => setTimeout(resolve, delay));
} else {
// logger.error(`Final attempt ${attempt} failed or error not retryable.`, { error: error.message, stack: error.stack }); // Use logger
throw error; // Re-throw the error if max retries reached or not retryable
}
}
}
}
// Inside the sendBulkSms loop:
// Replace: const response = await client.messages.create(payload);
// With: const response = await sendSingleBatchWithRetry(payload); // Assuming payload is defined correctly
- Why: Makes the service more resilient to temporary network or service issues without manual intervention.
- Important Caveats:
- Identifying Retryable Errors: The
isRetryable
check (statusCode >= 500 || statusCode === 429
) is a basic example. You must carefully examine the specific error codes and structures returned by the Plivo Node.js SDK for different failure scenarios (e.g., network issues, specific Plivo API errors) to determine accurately which errors are truly transient and safe to retry. Blindly retrying non-transient errors (like invalid credentials or insufficient funds) is wasteful and can hide underlying problems. - Error Structure: Relying solely on
statusCode
might not be sufficient. The Plivo SDK error object might contain more specific codes or messages within its body that are better indicators for retry logic. Thorough testing against Plivo's error responses is essential.
- Identifying Retryable Errors: The
6. Creating a Database Schema and Data Layer (Optional but Recommended)
While this guide focuses on stateless broadcasting, a production system often needs to:
- Manage recipient lists.
- Track broadcast campaigns.
- Store message status (sent, delivered, failed) using Plivo's Delivery Reports.
- Handle unsubscribes.
This typically requires a database.
Conceptual Schema (e.g., PostgreSQL):
-- broadcasts table
CREATE TABLE broadcasts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_text TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- e.g., 'pending', 'processing', 'completed', 'failed'
created_at TIMESTAMPTZ DEFAULT NOW(),
processed_at TIMESTAMPTZ
);
-- broadcast_recipients table
CREATE TABLE broadcast_recipients (
id BIGSERIAL PRIMARY KEY,
broadcast_id UUID NOT NULL REFERENCES broadcasts(id) ON DELETE CASCADE,
recipient_number VARCHAR(20) NOT NULL,
plivo_message_uuid VARCHAR(64) NULL UNIQUE, -- Stores the ID Plivo returns for tracking
status VARCHAR(20) NOT NULL DEFAULT 'queued', -- e.g., 'queued', 'sent', 'delivered', 'undelivered', 'failed'
status_updated_at TIMESTAMPTZ,
error_code VARCHAR(10) NULL, -- Plivo error code if failed
INDEX idx_broadcast_recipients_broadcast_id (broadcast_id),
INDEX idx_broadcast_recipients_status (status)
);
Implementation:
- Use an ORM (like Sequelize, Prisma, TypeORM) or a query builder (like Knex.js).
- Implement data access functions (e.g.,
createBroadcast
,addRecipients
,updateMessageStatus
). - Set up database migrations to manage schema changes.
- Create a webhook endpoint to receive Delivery Reports from Plivo and update the
broadcast_recipients
status. (Plivo Delivery Reports Docs)
This detailed database implementation is beyond the scope of this specific guide but is a critical next step for a full-featured production system.
7. Adding Security Features
Security is paramount. We've added basic auth, but consider these:
-
Input Validation & Sanitization: Already implemented using
express-validator
. Ensure validation is strict (e.g., tight regex for numbers). Ifmessage
content could ever come from user input, sanitize it to prevent XSS or injection attacks (though less likely for SMS). Libraries likeDOMPurify
(if rendering HTML somewhere) or basic escaping might be relevant depending on context. -
Rate Limiting (API Endpoint): Protect your own API from abuse.
// src/app.js (Add this) const rateLimit = require('express-rate-limit'); // Apply rate limiting to all requests or specific routes const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply the rate limiting middleware to all requests // Place it early, but potentially after static file serving if any app.use(limiter);
- Why: Prevents brute-force attacks or single users overwhelming your service. Adjust
windowMs
andmax
based on expected usage.
- Why: Prevents brute-force attacks or single users overwhelming your service. Adjust
-
HTTPS: Always use HTTPS in production. Deploy behind a reverse proxy (like Nginx or Caddy) or use hosting platforms (Heroku, Cloud Run) that handle TLS termination.
-
Helmet.js: Use the
helmet
middleware for setting various security-related HTTP headers.npm install helmet
// src/app.js (Add this near the top, after body-parser if used, before routes) const helmet = require('helmet'); app.use(helmet()); // Sets various security headers like Content-Security-Policy, X-Frame-Options etc.
-
Secrets Management: Use proper secrets management tools for API keys and database credentials in production (AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, Doppler). Do not store secrets directly in code or commit
.env
files.
8. Handling Special Cases
Real-world messaging has nuances:
- E.164 Format: Strictly enforce the E.164 format (
+
followed by country code and number, no spaces or dashes) for recipient numbers. Our validation helps, but ensure upstream data sources are clean. - Character Limits & Encoding: Standard SMS messages have a limit of 160 GSM-7 characters. Using non-GSM characters (like emojis or certain accented letters) switches encoding to UCS-2, reducing the limit to 70 characters per message part. Longer messages are split into multiple segments (concatenated SMS). Plivo handles segmentation, but be aware that a ""single"" long message might be billed as multiple messages by Plivo. Inform users or truncate messages if necessary.
- Sender ID Restrictions: US/Canada require using Plivo phone numbers (long codes or toll-free) as Sender IDs. Alphanumeric Sender IDs are supported in many other countries but often require pre-registration and may have restrictions (e.g., cannot receive replies). Check Plivo's documentation for the target countries.
- Invalid Numbers: Filter out known invalid numbers before sending. Handle Plivo errors related to invalid destinations gracefully (e.g., log them, potentially report back to the user).
- Opt-Outs/Compliance: Implement mechanisms to handle STOP/HELP keywords and maintain opt-out lists as required by regulations (e.g., TCPA in the US). Plivo offers features to help manage compliance. This often involves processing inbound messages or using Plivo's opt-out management features.
9. Implementing Performance Optimizations
For high-volume broadcasting:
- Batching: Already implemented – this is the most significant optimization for bulk sending via Plivo, reducing API call overhead.
- Asynchronous Processing: For very large lists (> thousands), making the API endpoint wait for all batches to complete can lead to client timeouts.
- Strategy: Accept the request, validate it, potentially store it in a database (see Section 6), and return a