Sending Short Message Service (SMS) messages remains a highly effective communication channel for alerts, notifications, marketing, and user verification. When you need to send messages to a large audience reliably and efficiently, building a dedicated bulk SMS system becomes essential.
This guide provides a step-by-step walkthrough for creating a robust bulk SMS sending application using Node.js, the Express framework, and the Vonage Messages API. We'll cover everything from project setup and core sending logic to rate limiting, error handling, security, and deployment considerations.
Project Goal: To build a Node.js Express API capable of accepting requests to send SMS messages individually and in bulk via the Vonage Messages API, incorporating best practices for reliability, scalability, and security.
Core Problem Solved: Efficiently and reliably sending a large volume of SMS messages programmatically without overwhelming the provider's API or incurring unnecessary delays, while also handling potential errors and tracking message statuses.
Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building scalable network applications.
- Express: A minimal and flexible Node.js web application framework for creating the API layer.
- Vonage Messages API: A unified API from Vonage for sending messages across various channels, including SMS. We'll use the
@vonage/server-sdk
Node.js library. dotenv
: A module to load environment variables from a.env
file intoprocess.env
.p-limit
: A utility library to limit concurrent promise executions, crucial for respecting API rate limits during bulk sends.- (Optional)
ngrok
: A tool to expose local servers to the internet for testing webhooks.
System Architecture:
graph TD
subgraph Your Application
A[API Client e.g., Postman/Frontend] --> B(Express API Server);
B -- Sends SMS Request --> C{Bulk SMS Service};
C -- Uses Vonage SDK --> D[Vonage Messages API];
C -- Logs Status/Errors --> E[Logging System];
F[Vonage Webhook] -- Sends Status Update --> B;
end
D -- Sends SMS --> G(User's Phone);
style B fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#ccf,stroke:#333,stroke-width:2px
Prerequisites:
- Node.js and npm (or yarn): Installed on your system. (Download Node.js)
- Vonage API Account: Sign up for free. You'll need your Application ID and Private Key. (Vonage Dashboard)
- Vonage Virtual Number: Rent an SMS-capable number from the Vonage dashboard.
- Vonage CLI (Optional but Recommended):
npm install -g @vonage/cli
ngrok
(Optional): For testing webhooks locally. (Download ngrok)- Basic understanding of Node.js, Express, REST APIs, and asynchronous JavaScript (Promises, async/await).
Expected Outcome: A functional Express API with endpoints to send single and bulk SMS messages, respecting Vonage rate limits, handling errors gracefully, and configured for security using environment variables.
1. Setting Up the Project
Let's initialize the Node.js project, install dependencies, and set up the basic structure.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir node-vonage-bulk-sms cd node-vonage-bulk-sms
-
Initialize npm Project: This creates a
package.json
file to manage dependencies and scripts.npm init -y
-
Install Dependencies: We need Express for the server, the Vonage SDK,
dotenv
for environment variables, andp-limit
for concurrency control.npm install express @vonage/server-sdk dotenv p-limit
-
Create Project Structure: Set up a basic structure for clarity.
mkdir src config touch src/server.js src/smsService.js config/vonageClient.js .env .env.example .gitignore
src/server.js
: Main Express application setup and routes.src/smsService.js
: Logic for interacting with the Vonage API.config/vonageClient.js
: Initializes and configures the Vonage SDK client..env
: Stores sensitive credentials (API keys, etc.). Do not commit this file..env.example
: A template showing required environment variables. Commit this file..gitignore
: Specifies intentionally untracked files that Git should ignore (like.env
andnode_modules
).
-
Configure
.gitignore
: Add the following to your.gitignore
file to prevent committing sensitive data and unnecessary files:# Dependencies node_modules/ # Environment variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log*
-
Set Up Environment Variables: Open
.env.example
and add the following placeholders:# .env.example # Vonage API Credentials (Messages API - Use Application ID & Private Key for this guide) VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key # Or the full path to your key file VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # In E.164 format, e.g., 12015550123 # Server Configuration PORT=3000 # Bulk Sending Configuration VONAGE_CONCURRENCY_LIMIT=5 # Adjust based on your Vonage account limits (e.g., 1 for Long Code, up to 30 for Toll-Free/Short Code after approval)
Now, create the actual
.env
file by copying.env.example
. Fill in your actual Vonage credentials and number in the.env
file.VONAGE_APPLICATION_ID
&VONAGE_PRIVATE_KEY_PATH
: These are required for the Messages API authentication used in this guide.- Go to your Vonage API Dashboard.
- Click ""Create a new application"".
- Give it a name (e.g., ""Bulk SMS Sender"").
- Enable the ""Messages"" capability. You'll need to provide webhook URLs later if you want status updates or inbound messages (e.g.,
https://your-domain.com/webhooks/status
andhttps://your-domain.com/webhooks/inbound
). For now, you can use placeholders likehttp://localhost:3000/status
andhttp://localhost:3000/inbound
. - Click ""Generate public and private key"". Save the
private.key
file that downloads – place it in your project root (or specify the correct path in.env
). - Note the Application ID displayed on the page.
- Link your Vonage virtual number to this application under the ""Link numbers"" section.
VONAGE_NUMBER
: Your SMS-capable virtual number purchased from Vonage, in E.164 format (e.g.,14155552671
).VONAGE_CONCURRENCY_LIMIT
: Start low (e.g., 1-5) and adjust based on your number type (Long Code – 1 SMS/sec, Toll-Free/Short Code – 10-30+ SMS/sec after registration/approval) and Vonage account limits. Check Vonage documentation and potentially contact support for higher limits.
-
Configure Vonage Account for Messages API: It's crucial to ensure your Vonage account uses the Messages API for sending SMS by default when authenticating with Application ID / Private Key.
- Go to your Vonage API Dashboard Settings.
- Scroll down to ""API Settings"".
- Under ""Default SMS Setting"", ensure ""Messages API"" is selected.
- Click ""Save changes"".
2. Implementing Core Functionality (Sending SMS)
Now, let's set up the Vonage client and create the service function to send a single SMS.
-
Initialize Vonage Client (
config/vonageClient.js
): This module initializes the SDK using credentials from.env
.// config/vonageClient.js require('dotenv').config(); // Load .env variables const { Vonage } = require('@vonage/server-sdk'); const path = require('path'); // Ensure the path is resolved correctly relative to the project root const privateKeyPath = path.resolve(process.env.VONAGE_PRIVATE_KEY_PATH || './private.key'); if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) { console.error('Error: Vonage Application ID or Private Key Path not set in .env file.'); console.error('Please ensure VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH are defined.'); // Optionally exit or throw an error in a real app // process.exit(1); } const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKeyPath, // Use the resolved path }, { debug: false }); // Set debug: true for verbose SDK logging module.exports = vonage;
- We load environment variables using
dotenv
. - We resolve the
privateKey
path to ensure it works regardless of where the script is run from. - We initialize
Vonage
using the Application ID and Private Key, suitable for the Messages API. - Basic error checking ensures required environment variables are present.
- We load environment variables using
-
Create SMS Sending Service (
src/smsService.js
): This module contains the logic for sending SMS messages.// src/smsService.js const vonage = require('../config/vonageClient'); const pLimit = require('p-limit'); const fromNumber = process.env.VONAGE_NUMBER; const concurrencyLimit = parseInt(process.env.VONAGE_CONCURRENCY_LIMIT || '5', 10); // Initialize p-limit with the desired concurrency const limit = pLimit(concurrencyLimit); /** * Sends a single SMS message using Vonage Messages API. * @param {string} to - The recipient phone number in E.164 format. * @param {string} text - The message content. * @returns {Promise<object>} - Promise resolving with Vonage API response or rejecting with a structured error. */ async function sendSingleSms(to, text) { if (!fromNumber) { throw new Error('VONAGE_NUMBER is not defined in the environment variables.'); } if (!to || !text) { throw new Error('Recipient number (to) and message text cannot be empty.'); } console.log(`Attempting to send SMS to ${to} from ${fromNumber}`); try { // Use the vonage.messages.send method for Messages API const response = await vonage.messages.send({ message_type: ""text"", text: text, to: to, from: fromNumber, channel: ""sms"" }); console.log(`SMS submitted to Vonage for ${to}. Message UUID: ${response.message_uuid}`); return { success: true, message_uuid: response.message_uuid, recipient: to }; } catch (error) { console.error(`Error sending SMS to ${to}:`, error.response ? error.response.data : error.message); // Rethrow or return a structured error object throw { success: false, recipient: to, error: error.message, details: error.response?.data }; } } /** * Sends SMS messages in bulk, respecting concurrency limits. * @param {string[]} recipients - Array of recipient phone numbers in E.164 format. * @param {string} text - The message content. * @returns {Promise<object[]>} - Promise resolving with an array of results for each message. */ async function sendBulkSms(recipients, text) { if (!Array.isArray(recipients) || recipients.length === 0) { throw new Error('Recipients must be a non-empty array.'); } if (!text) { throw new Error('Message text cannot be empty.'); } console.log(`Starting bulk SMS send to ${recipients.length} recipients. Concurrency limit: ${concurrencyLimit}`); // Create an array of promises, wrapped by p-limit const sendPromises = recipients.map(recipient => limit(() => sendSingleSms(recipient, text)) .catch(error => { // Ensure even rejected promises return a structured error console.error(`Caught error during bulk send for ${recipient}: ${error.error || error.message}`); return { success: false, recipient: recipient, error: error.error || error.message, details: error.details }; }) ); // Execute all promises concurrently (up to the limit) const results = await Promise.all(sendPromises); console.log(`Bulk SMS processing complete. Results count: ${results.length}`); return results; } module.exports = { sendSingleSms, sendBulkSms, };
- We import the configured
vonage
client andp-limit
. sendSingleSms
: Takesto
andtext
, performs basic validation, and usesvonage.messages.send
(correct method for the Messages API). It logs success/error and returns a structured result.sendBulkSms
: Takes an array ofrecipients
andtext
.- It initializes
pLimit
with theVONAGE_CONCURRENCY_LIMIT
from.env
. - It maps each recipient to a promise generated by calling
sendSingleSms
. Crucially, each call is wrapped inlimit()
. This ensures that no more thanconcurrencyLimit
promises (API calls) are active simultaneously. Promise.all
waits for all limited promises to settle (resolve or reject).- A
.catch
is added within the map to handle individual failures gracefully without stopping the entire batch, ensuringPromise.all
receives a result (success or structured error) for every recipient.
- It initializes
- We import the configured
3. Building the API Layer
Let's create the Express server and define API endpoints to trigger the SMS sending functions.
-
Create Express Server (
src/server.js
):// src/server.js require('dotenv').config(); // Load .env variables first const express = require('express'); const path = require('path'); // Import path module const { sendSingleSms, sendBulkSms } = require('./smsService'); const app = express(); const port = process.env.PORT || 3000; // Middleware to parse JSON request bodies app.use(express.json()); // Middleware to parse URL-encoded request bodies app.use(express.urlencoded({ extended: true })); // Simple Health Check Endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() }); }); // === API Endpoints === /** * @route POST /api/sms/send * @description Send a single SMS message. * @access Public (Add Auth Middleware in Production) * @requestBody { ""to"": ""RECIPIENT_NUMBER_E164"", ""text"": ""Your message content"" } * @response 200 { ""success"": true, ""message_uuid"": ""..."", ""recipient"": ""..."" } * @response 400 { ""success"": false, ""error"": ""Validation message"" } * @response 500 { ""success"": false, ""error"": ""Server/API error message"", ""details"": ""..."" } */ app.post('/api/sms/send', async (req, res) => { const { to, text } = req.body; // Basic Input Validation if (!to || !text) { return res.status(400).json({ success: false, error: 'Missing required fields: to, text' }); } // Add more robust validation (e.g., E.164 format check for 'to') if needed try { const result = await sendSingleSms(to, text); res.status(200).json(result); } catch (error) { console.error('API Error sending single SMS:', error); res.status(error.details ? 400 : 500).json({ // Use 400 for Vonage API errors, 500 for others success: false, error: error.error || 'Failed to send SMS', details: error.details // Include details from Vonage if available }); } }); /** * @route POST /api/sms/bulk-send * @description Send SMS messages to multiple recipients. * @access Public (Add Auth Middleware in Production) * @requestBody { ""recipients"": [""NUMBER1_E164"", ""NUMBER2_E164""], ""text"": ""Your message content"" } * @response 200 { ""totalSubmitted"": N, ""results"": [ { ""success"": true/false, ""recipient"": ""..."", ... } ] } * @response 400 { ""success"": false, ""error"": ""Validation message"" } * @response 500 { ""success"": false, ""error"": ""Initial error before processing"" } */ app.post('/api/sms/bulk-send', async (req, res) => { const { recipients, text } = req.body; // Basic Input Validation if (!Array.isArray(recipients) || recipients.length === 0 || !text) { return res.status(400).json({ success: false, error: 'Missing or invalid fields: recipients (non-empty array), text' }); } // Add more robust validation per recipient if needed try { console.log(`API request received for bulk send to ${recipients.length} recipients.`); const results = await sendBulkSms(recipients, text); res.status(200).json({ totalSubmitted: recipients.length, results: results }); } catch (error) { // This catches errors *before* bulk processing starts (e.g., validation) console.error('API Error initiating bulk SMS send:', error); res.status(500).json({ success: false, error: error.message || 'Failed to initiate bulk SMS send' }); } }); // === Webhook Endpoint (Optional - for Status Updates) === /** * @route POST /webhooks/status * @description Receives delivery status updates from Vonage. * @access Public (Needs security in production) */ app.post('/webhooks/status', (req, res) => { console.log('Received Status Webhook:', JSON.stringify(req.body, null, 2)); // TODO: Process the status update (e.g., update database record) // Status examples: submitted, delivered, rejected, undeliverable const { message_uuid, status, timestamp, to, from, error } = req.body; console.log(`Status for UUID ${message_uuid}: ${status} at ${timestamp}`); if (error) { console.error(`Error details: Code ${error.code}, Reason: ${error.reason}`); } // Vonage expects a 200 OK response quickly to prevent retries res.status(200).end(); }); // === Start Server === app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); console.log(`Bulk SMS concurrency limit set to: ${process.env.VONAGE_CONCURRENCY_LIMIT || 5}`); console.log(`Ensure VONAGE_NUMBER (${process.env.VONAGE_NUMBER}) is linked to Application ID ${process.env.VONAGE_APPLICATION_ID} in Vonage Dashboard.`); // Log the resolved path to help debug potential path issues const privateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH || './private.key'; console.log(`Using Private Key Path: ${path.resolve(privateKeyPath)}`); });
- We set up a basic Express app, including JSON and URL-encoded body parsers.
/api/sms/send
: Handles single sends, callssendSingleSms
, and returns the result or an error. Includes basic validation./api/sms/bulk-send
: Handles bulk sends, validates input, callssendBulkSms
, and returns an array containing the outcome for each recipient./webhooks/status
: A basic endpoint to log incoming delivery status updates from Vonage. Important: In production, this needs security (e.g., signature verification) and logic to process the status.- The server starts listening on the configured
PORT
.
-
Add Start Script to
package.json
:{ ""name"": ""node-vonage-bulk-sms"", ""version"": ""1.0.0"", ""description"": ""Bulk SMS sender using Node.js, Express, and Vonage"", ""main"": ""src/server.js"", ""scripts"": { ""start"": ""node src/server.js"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" }, ""keywords"": [ ""vonage"", ""sms"", ""bulk"", ""node"", ""express"" ], ""author"": """", ""license"": ""ISC"", ""dependencies"": { ""@vonage/server-sdk"": ""^3.13.1"", ""dotenv"": ""^16.4.5"", ""express"": ""^4.19.2"", ""p-limit"": ""^4.0.0"" } }
- We added a
start
script to easily run the server. - Dependency versions are illustrative; use actual installed versions.
- We added a
-
Run the Application:
npm start
You should see output indicating the server is running and confirming the configuration.
-
Test API Endpoints:
Use
curl
or a tool like Postman. Replace placeholders with your actual Vonage number and a recipient number you can test with.-
Send Single SMS:
curl -X POST http://localhost:3000/api/sms/send \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""RECIPIENT_NUMBER_E164"", ""text"": ""Hello from single send API!"" }'
Expected Response (Success):
{ ""success"": true, ""message_uuid"": ""a1b2c3d4-e5f6-7890-1234-567890abcdef"", ""recipient"": ""RECIPIENT_NUMBER_E164"" }
-
Send Bulk SMS:
curl -X POST http://localhost:3000/api/sms/bulk-send \ -H ""Content-Type: application/json"" \ -d '{ ""recipients"": [""RECIPIENT_NUMBER_1"", ""RECIPIENT_NUMBER_2"", ""INVALID_NUMBER_FORMAT""], ""text"": ""Bulk message test!"" }'
Expected Response (Mixed Results):
{ ""totalSubmitted"": 3, ""results"": [ { ""success"": true, ""message_uuid"": ""uuid-for-recipient-1"", ""recipient"": ""RECIPIENT_NUMBER_1"" }, { ""success"": true, ""message_uuid"": ""uuid-for-recipient-2"", ""recipient"": ""RECIPIENT_NUMBER_2"" }, { ""success"": false, ""recipient"": ""INVALID_NUMBER_FORMAT"", ""error"": ""Bad Request: The `to` parameter is invalid."", ""details"": { /* Vonage error details */ } } ] }
-
4. Integrating Third-Party Services (Vonage Configuration Recap)
We've already integrated Vonage, but let's recap the essential configuration points:
-
API Credentials:
- Stored securely in
.env
. - Using Application ID and Private Key for the Messages API as demonstrated in this guide.
- Obtained from the Vonage Dashboard -> Applications section after creating an application and generating keys.
- The
private.key
file path must be correctly specified in.env
and accessible by the application.
- Stored securely in
-
Virtual Number (
VONAGE_NUMBER
):- Must be an SMS-capable number rented from Vonage.
- Must be linked to the Vonage Application used (via Application ID) in the dashboard settings. Failure to link results in authentication errors.
- Specified in E.164 format in
.env
.
-
Default SMS Setting:
- Crucially, ensure ""Messages API"" is set as the default SMS handler in Vonage Dashboard -> Settings -> API Settings. If ""SMS API"" is selected, the authentication (API Key/Secret) and SDK methods (
vonage.sms.send
) would differ.
- Crucially, ensure ""Messages API"" is set as the default SMS handler in Vonage Dashboard -> Settings -> API Settings. If ""SMS API"" is selected, the authentication (API Key/Secret) and SDK methods (
-
Webhooks (Status/Inbound - Optional but Recommended):
- Status URL: Configured in the Vonage Application settings (e.g.,
https://your-deployed-app.com/webhooks/status
). Vonage sends POST requests here with delivery updates (delivered
,failed
,rejected
, etc.). - Inbound URL: Configured similarly (e.g.,
https://your-deployed-app.com/webhooks/inbound
) if you need to receive SMS messages sent to your Vonage number. - Local Testing: Use
ngrok
to expose your localserver.js
port (e.g.,ngrok http 3000
). Use the generatedhttps://*.ngrok.io
URL (appending/webhooks/status
or/webhooks/inbound
) in the Vonage dashboard for testing webhook functionality locally. Remember to update the URLs when deploying.
- Status URL: Configured in the Vonage Application settings (e.g.,
5. Error Handling, Logging, and Retries
Our current setup includes basic error handling and logging. Let's refine it.
-
Consistent Error Handling:
- The
smsService.js
functionsthrow
structured error objects on failure, includingsuccess: false
,recipient
,error
message, and optionaldetails
from the Vonage API response. - The API endpoints in
server.js
catch these errors and return appropriate HTTP status codes (e.g., 400 for validation/API errors, 500 for unexpected server errors) and JSON error responses.
- The
-
Logging:
- We use
console.log
for informational messages (sending attempts, success UUIDs, webhook reception) andconsole.error
for errors. - Production Enhancement: Replace
console.log/error
with a dedicated logging library likewinston
orpino
. This enables:- Different log levels (debug, info, warn, error).
- Structured logging (JSON format for easier parsing).
- Outputting logs to files, databases, or external logging services (like Datadog, Logstash).
- Example (Conceptual Winston Setup):
// config/logger.js (Conceptual) const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), // Add file transport for production // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }) ], }); module.exports = logger; // Usage in other files: // const logger = require('../config/logger'); // logger.info(`Sending SMS to ${to}`); // logger.error(`Error sending SMS: ${error.message}`, { error });
- We use
-
Retry Mechanisms:
- Vonage's API might occasionally fail due to temporary network issues or rate limiting. Implementing retries can improve reliability.
- Strategy: Use libraries like
async-retry
or implement a custom loop with exponential backoff for specific, retryable errors (e.g., network errors,429 Too Many Requests
). Avoid retrying non-recoverable errors (like invalid number format400
). - Example (Conceptual Retry in
sendSingleSms
):Remember to adjust// src/smsService.js (Conceptual Retry Snippet) const retry = require('async-retry'); // npm install async-retry async function sendSingleSmsWithRetry(to, text) { // ... (validation as before) ... return retry(async (bail, attempt) => { console.log(`Attempt ${attempt}: Sending SMS to ${to}`); try { const response = await vonage.messages.send({ /* ... params ... */ }); console.log(`Success (Attempt ${attempt}) for ${to}. UUID: ${response.message_uuid}`); return { success: true, message_uuid: response.message_uuid, recipient: to }; } catch (error) { const statusCode = error.response?.status; const isRetryable = statusCode === 429 || statusCode >= 500; // Example: Retry on rate limit or server errors if (!isRetryable) { console.error(`Non-retryable error for ${to} (Attempt ${attempt}):`, error.response?.data || error.message); // bail() tells async-retry to stop retrying immediately and reject with the error passed to bail. bail(new Error(`Non-retryable error: ${error.message}`)); // NOTE: Even though bail is called, the code execution continues. // It's best practice to throw the structured error here so the caller gets the consistent error format. // The promise returned by retry() will be rejected with the error passed to bail(). throw { success: false, recipient: to, error: error.message, details: error.response?.data }; } else { console.warn(`Retryable error for ${to} (Attempt ${attempt}): Status ${statusCode}. Retrying...`); // Throwing the error signals async-retry to perform the next retry attempt. throw error; } } }, { retries: 2, // Number of retries (total attempts = retries + 1) factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial delay in ms onRetry: (error, attempt) => { console.warn(`Retrying SMS to ${to}. Attempt ${attempt} failed: ${error.message}`); } }); }
sendBulkSms
to callsendSingleSmsWithRetry
if you implement this.
6. Database Schema and Data Layer (Optional Extension)
For tracking message status, managing large campaigns, or storing recipient lists, integrating a database is necessary.
-
Schema Design (Conceptual - e.g., PostgreSQL): You might have tables like:
sms_messages
:id
(PK, UUID or Serial)vonage_message_uuid
(VARCHAR, UNIQUE, Index) - Received from Vonage on submissionrecipient_number
(VARCHAR, Index)sender_number
(VARCHAR)message_text
(TEXT)status
(VARCHAR, Index - e.g., 'submitted', 'delivered', 'failed', 'rejected', 'undeliverable') - Updated via webhooksubmitted_at
(TIMESTAMPZ)last_updated_at
(TIMESTAMPZ) - Updated via webhookerror_code
(VARCHAR, Nullable)error_reason
(TEXT, Nullable)campaign_id
(FK, Nullable) - Link to a potential campaigns table
sms_campaigns
(Optional):id
(PK)name
(VARCHAR)scheduled_at
(TIMESTAMPZ, Nullable)status
(VARCHAR - e.g., 'pending', 'processing', 'complete', 'failed')created_at
(TIMESTAMPZ)
erDiagram SMS_MESSAGES ||--o{ SMS_CAMPAIGNS : belongs_to SMS_MESSAGES { UUID id PK VARCHAR vonage_message_uuid UK ""Index"" VARCHAR recipient_number ""Index"" VARCHAR sender_number TEXT message_text VARCHAR status ""Index, e.g., 'submitted', 'delivered'"" TIMESTAMPZ submitted_at TIMESTAMPZ last_updated_at VARCHAR error_code NULL TEXT error_reason NULL UUID campaign_id FK NULL } SMS_CAMPAIGNS { UUID id PK VARCHAR name TIMESTAMPZ scheduled_at NULL VARCHAR status ""e.g., 'pending', 'complete'"" TIMESTAMPZ created_at }
-
Data Access Layer:
- Use an ORM (like Sequelize, Prisma, TypeORM) or a query builder (like Knex.js) to interact with the database.
- Modify
smsService.js
:- Before sending, insert a record into
sms_messages
with status 'pending' or 'submitted'. - On successful submission to Vonage, update the record with the
vonage_message_uuid
and set status to 'submitted'. - On submission error, update the record with status 'failed' and error details.
- Before sending, insert a record into
- Modify
/webhooks/status
handler:- Find the
sms_messages
record using the incomingmessage_uuid
. - Update the
status
,last_updated_at
,error_code
,error_reason
based on the webhook payload.
- Find the
-
Migrations: Use the migration tools provided by your chosen ORM or query builder (e.g.,
sequelize-cli
,prisma migrate
,knex migrate
) to manage database schema changes reliably.