This guide provides a step-by-step walkthrough for building a robust bulk SMS broadcasting system using Node.js, Express, and the Vonage Messages API. We'll cover everything from project setup and core Vonage integration to essential production considerations like rate limiting, error handling, security, and deployment.
By the end of this guide, you will have a functional Node.js application capable of sending SMS messages to a list of recipients via an API endpoint, incorporating best practices for reliability and scalability.
Project Overview and Goals
What We're Building:
We will construct a Node.js application using the Express framework that exposes an API endpoint. This endpoint will accept a list of phone numbers and a message text, then use the Vonage Messages API to broadcast the SMS message to all specified recipients efficiently and reliably.
Problem Solved:
This system addresses the need to send the same SMS message to multiple recipients simultaneously (e.g., notifications, alerts, marketing campaigns) while managing API rate limits, tracking message status, and ensuring secure handling of credentials.
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 choose the Messages API over the older SMS API for its modern features, better support for different channels, and detailed status webhooks.
dotenv
: A zero-dependency module that loads environment variables from a.env
file intoprocess.env
.@vonage/server-sdk
: The official Vonage Server SDK for Node.js, simplifying interaction with Vonage APIs.ngrok
(for development): A tool to expose local servers to the internet, necessary for testing Vonage webhooks.
System Architecture:
(A diagram illustrating the system architecture would typically be placed here, showing client, Node.js app, Vonage API, Mobile Network, and Recipient interactions, including the webhook flow.)
Prerequisites:
- A Vonage API account.
- Node.js and npm (or yarn) installed locally.
- Basic understanding of JavaScript, Node.js, and REST APIs.
ngrok
installed for local webhook testing (Download here).- Vonage CLI (Optional but helpful for managing applications/numbers): Can be run on-demand using
npx @vonage/cli [...]
or installed globally (npm install -g @vonage/cli
), which may require administrator privileges.
Final Outcome:
A Node.js Express application with:
- An API endpoint (
/broadcast
) to trigger bulk SMS sends. - Integration with the Vonage Messages API using the Node.js SDK.
- Basic rate limiting to comply with Vonage API constraints.
- Environment variable management for secure credential handling.
- Webhook endpoints to receive message status updates from Vonage.
- Logging for monitoring and troubleshooting.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
Step 1: Create Project Directory
Open your terminal and create a new directory for the project, then navigate into it.
mkdir vonage-bulk-sms
cd vonage-bulk-sms
Step 2: Initialize Node.js Project
Initialize the project using npm (or yarn). This creates a package.json
file. The -y
flag accepts the default settings.
npm init -y
Step 3: Install Dependencies
Install Express for the web server, the Vonage Server SDK, and dotenv
for environment variables.
npm install express @vonage/server-sdk dotenv
Step 4: Set Up Project Structure
Create the basic files and folders for our application.
touch index.js .env .gitignore
Your basic structure should look like this:
vonage-bulk-sms/
├── node_modules/
├── .env
├── .gitignore
├── index.js
└── package.json
Step 5: Configure .gitignore
Add node_modules
and .env
to your .gitignore
file to prevent committing sensitive information and dependencies to version control.
node_modules
.env
Step 6: Configure Environment Variables (.env
)
Create a .env
file in the root of your project. This file will store your Vonage credentials and other configuration securely. We will fill in the actual values later.
# Vonage Credentials & Config
VONAGE_API_KEY=YOUR_VONAGE_API_KEY
VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
VONAGE_FROM_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
# Application Settings
PORT=3000
BASE_URL=http://localhost:3000 # For local testing, will be replaced by ngrok URL for webhooks
Why this setup?
npm init
: Standard way to start a Node.js project, managing dependencies and scripts.- Dependencies: Express is a standard for Node.js web apps;
@vonage/server-sdk
simplifies Vonage interaction;dotenv
is crucial for securely managing API keys outside of code. .gitignore
: Prevents accidental exposure of secrets (.env
) and unnecessary bloat (node_modules
) in Git..env
: Isolates configuration and secrets from the codebase, making it easier to manage different environments (development, production) and enhancing security.
2. Implementing Core Functionality (Vonage Integration)
Now, let's integrate the Vonage SDK and implement the logic to send SMS messages.
Step 1: Initialize Express and Vonage SDK
Open index.js
and set up the basic Express server and initialize the Vonage SDK using the environment variables.
// index.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const fs = require('fs'); // Needed to read the private key file
const app = express();
app.use(express.json()); // Middleware to parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Middleware for URL-encoded bodies
const PORT = process.env.PORT || 3000;
const VONAGE_FROM_NUMBER = process.env.VONAGE_FROM_NUMBER;
// --- Vonage Initialization ---
// Check if required Vonage environment variables are set
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !VONAGE_FROM_NUMBER) {
console.error('Error: Missing required Vonage environment variables in .env file.');
process.exit(1); // Exit if configuration is missing
}
let vonage;
try {
// Read the private key
const privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKey // Use the file content directly
}, {
// Optional: Add custom user agent for tracking/debugging
appendToUserAgent: 'vonage-bulk-sms-guide/1.0.0'
});
console.log('Vonage SDK initialized successfully.');
} catch (error) {
console.error('Error initializing Vonage SDK:', error);
if (error.code === 'ENOENT') {
console.error(`Private key file not found at path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`);
}
process.exit(1);
}
// --- Basic Root Route ---
app.get('/', (req, res) => {
res.send('Vonage Bulk SMS Service is running!');
});
// --- Start Server ---
// Export server for testing purposes
const server = app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`Base URL: ${process.env.BASE_URL}`);
console.log(`Ensure BASE_URL matches your ngrok forwarding URL for webhooks.`);
});
// --- Placeholder for other routes (API, Webhooks) ---
// We will add these later
// Export app and server for testing
module.exports = { app, server, vonage }; // Export vonage if needed for mocking in tests
require('dotenv').config();
: Must be called early to load variables.express.json()
/express.urlencoded()
: Necessary middleware to parse incoming request bodies.- Vonage Initialization: We use the
Vonage
class from the SDK, providing credentials and the content of the private key file read usingfs.readFileSync
. Using the Messages API requires an Application ID and Private Key for authentication. - Error Handling: Added checks for missing environment variables and errors during SDK initialization (like a missing private key file).
Step 2: Implement Single SMS Sending Function
Create a reusable function to send a single SMS message. This helps structure the code. (This version includes enhanced logging).
// index.js (add this function)
// --- Function to Send Single SMS (with enhanced logging) ---
async function sendSms(toNumber, messageText) {
const logPrefix = `[SMS to ${toNumber}]`;
console.log(`${logPrefix} Attempting to send...`);
try {
const resp = await vonage.messages.send({
message_type: ""text"",
text: messageText,
to: toNumber,
from: VONAGE_FROM_NUMBER, // Must be a Vonage virtual number linked to your application
channel: ""sms""
});
console.log(`${logPrefix} Submitted successfully. UUID: ${resp.messageUuid}`);
return { success: true, messageUuid: resp.messageUuid, recipient: toNumber };
} catch (err) {
// Extract more specific error details if available
const errorMessage = err.response?.data?.title || err.response?.data?.detail || err.message || 'Unknown error';
// Try to get a numeric status code, or a specific Vonage code, or 'N/A'
const errorCode = err.response?.status?.toString() || err.response?.data?.invalid_parameters?.[0]?.name || 'N/A';
console.error(`${logPrefix} Submission failed. Code: ${errorCode}, Reason: ${errorMessage}`);
// Log the full error object for deeper debugging if needed
// console.error(`${logPrefix} Full error object:`, JSON.stringify(err.response?.data || err, null, 2));
return { success: false, error: errorMessage, errorCode: errorCode, recipient: toNumber };
}
}
vonage.messages.send
: The core method from the SDK for the Messages API.- Parameters:
message_type
,text
,to
,from
, andchannel
are required for SMS. async/await
: Used for cleaner handling of the asynchronous API call.- Return Value: Returns an object indicating success/failure, including the
messageUuid
for tracking anderrorCode
on failure. - Enhanced Logging: Provides more context in logs and attempts to extract meaningful error codes/messages.
Step 3: Implement Basic Rate Limiting (Delay)
Vonage imposes rate limits. Actual throughput depends heavily on the number type (Long Code, Toll-Free, Short Code) and mandatory US A2P 10DLC registration status/campaign type. Check Vonage docs and your 10DLC registration for specifics. A simple delay between sends is a basic starting point but likely insufficient/incorrect for many use cases without verification.
// index.js (add this helper function)
// --- Helper function for delays ---
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Define delay in milliseconds.
// WARNING: 1100ms (~1 SMS/sec) is a VERY basic starting point, often for standard US Long Codes *without* 10DLC registration (which will be blocked/filtered).
// Your ACTUAL required delay depends entirely on your Vonage number type (Long Code, Toll-Free, Short Code),
// US A2P 10DLC registration status and approved throughput, and target country regulations.
// EXCEEDING YOUR ALLOWED RATE WILL LEAD TO ERRORS (429) AND MESSAGE BLOCKING.
// Consult Vonage documentation and your account details for appropriate limits.
const SEND_DELAY_MS = 1100; // EXAMPLE ONLY - ADJUST BASED ON YOUR ACTUAL THROUGHPUT LIMITS
Why this approach?
- Modularity: Separating the
sendSms
function makes the code cleaner and reusable. - Messages API: Chosen for its modern features and webhook capabilities.
- Rate Limiting (Simple Delay): The
sleep
function provides a basic mechanism to avoid hitting simple per-second limits. Crucially, this is an oversimplification. Production systems sending moderate to high volume, especially in regulated markets like the US (A2P 10DLC), must account for specific throughput limits tied to number types and registration. A simple fixed delay is often inadequate. Consider concurrent sending within verified limits (see Section 5/6) or a background job queue for better management.
3. Building a Complete API Layer
Let's create the /broadcast
endpoint to trigger the bulk sending process.
Step 1: Define the /broadcast
Route
Add a POST route in index.js
that accepts a list of numbers and a message.
// index.js (add this route handler)
// --- API Endpoint for Bulk Broadcasting ---
app.post('/broadcast', async (req, res) => {
const { recipients, message } = req.body;
// --- Input Validation ---
if (!recipients || !Array.isArray(recipients) || recipients.length === 0) {
return res.status(400).json({ error: 'Invalid or missing "recipients" array in request body.' });
}
if (!message || typeof message !== 'string' || message.trim() === '') {
return res.status(400).json({ error: 'Invalid or missing "message" string in request body.' });
}
// Basic phone number format validation.
// WARNING: This regex is very basic and NOT recommended for production.
// It doesn't validate E.164 format (missing '+') or handle international variations well.
// Use a dedicated library like 'libphonenumber-js' for robust validation.
const phoneRegex = /^\d{10,15}$/;
const invalidNumbers = recipients.filter(num => !phoneRegex.test(num.toString()));
if (invalidNumbers.length > 0) {
console.warn(`Warning: Potential invalid phone numbers detected (based on basic check): ${invalidNumbers.join(', ')}`);
// Decide whether to reject or proceed. For production, stricter validation before this point is better.
// return res.status(400).json({ error: 'Invalid phone number format detected.', invalidNumbers });
}
console.log(`Received broadcast request for ${recipients.length} recipients.`);
console.log(`Message: "${message}"`);
// --- Process Recipients Sequentially with Delay ---
const results = [];
let successCount = 0;
let failureCount = 0;
for (const recipient of recipients) {
const result = await sendSms(recipient, message);
results.push(result);
if (result.success) {
successCount++;
} else {
failureCount++;
}
// Wait before sending the next message to respect rate limits (see caveats in Section 2)
await sleep(SEND_DELAY_MS);
}
console.log(`Broadcast attempt finished. Success: ${successCount}, Failure: ${failureCount}`);
// --- Respond to Client ---
// Respond with initial submission results. Actual delivery status comes via webhook.
// Status 202 implies processing continues, but here we wait for the loop.
// For true async, respond earlier and use background jobs (See Section 6).
res.status(200).json({ // Changed to 200 OK as we wait for the loop here. Use 202 if responding before loop.
message: `Broadcast submission process completed for ${recipients.length} recipients. Check webhook or logs for delivery status.`,
summary: {
totalRecipients: recipients.length,
submittedSuccessfully: successCount,
submissionFailures: failureCount,
},
details: results // Provides per-recipient submission status
});
});
- Input Validation: Checks for the presence and basic format of
recipients
andmessage
. Includes a very basic regex for phone numbers with a strong recommendation to use a proper library likelibphonenumber-js
for production. - Sequential Processing: Iterates through the
recipients
array. await sendSms(...)
: Calls our SMS sending function for each recipient.await sleep(...)
: Pauses execution between each send attempt, subject to the caveats mentioned earlier.- Response: Returns a
200 OK
status after attempting all submissions in the loop. Includes a summary and detailed results of the submission attempts.
Step 2: Testing the Endpoint (Example using curl
)
Make sure your server is running (node index.js
). Open another terminal and run the following command (replace YOUR_TEST_PHONE_NUMBER_...
with actual E.164 formatted numbers):
curl -X POST http://localhost:3000/broadcast \
-H "Content-Type: application/json" \
-d '{
"recipients": ["YOUR_TEST_PHONE_NUMBER_1", "YOUR_TEST_PHONE_NUMBER_2"],
"message": "Hello from the Bulk SMS Broadcaster!"
}'
Expected JSON Response (example):
{
"message": "Broadcast submission process completed for 2 recipients. Check webhook or logs for delivery status.",
"summary": {
"totalRecipients": 2,
"submittedSuccessfully": 2,
"submissionFailures": 0
},
"details": [
{ "success": true, "messageUuid": "...", "recipient": "...", "errorCode": null },
{ "success": true, "messageUuid": "...", "recipient": "...", "errorCode": null }
]
}
(The actual success
, messageUuid
, recipient
, and errorCode
values will reflect the outcome of the submission attempts.)
You should also see logs in your server console and receive the SMS messages on the test phones (after the specified delay).
4. Integrating with Vonage (Configuration & Webhooks)
Proper configuration in the Vonage Dashboard and setting up webhooks are crucial for the Messages API.
Step 1: Obtain Vonage Credentials
- Log in to your Vonage API Dashboard.
- Your API Key and API Secret are displayed on the main dashboard page. Copy these into your
.env
file.
Step 2: Create a Vonage Application
The Messages API requires an Application to handle authentication (via private key) and webhooks.
- Navigate to ""Applications"" in the dashboard sidebar.
- Click ""Create a new application"".
- Give it a name (e.g., ""Bulk SMS Broadcaster"").
- Click ""Generate public and private key"". Immediately save the
private.key
file that downloads. Place this file in your project's root directory (or updateVONAGE_PRIVATE_KEY_PATH
in.env
if you save it elsewhere). The public key is stored by Vonage. - Enable the ""Messages"" capability.
- You'll need to provide two webhook URLs (we'll get the
YOUR_NGROK_URL
part in Step 4):- Inbound URL: Receives incoming messages sent to your Vonage number. Define it as
YOUR_NGROK_URL/webhooks/inbound
. - Status URL: Receives status updates (delivered, failed, etc.) for messages you send. This is critical. Define it as
YOUR_NGROK_URL/webhooks/status
.
- Inbound URL: Receives incoming messages sent to your Vonage number. Define it as
- Click ""Generate new application"".
- Copy the generated Application ID into your
.env
file.
Step 3: Link a Vonage Number
- Go back to the Application details page (Applications -> Your Application Name).
- Under ""Linked numbers"", click ""Link"" and choose one of your Vonage virtual numbers capable of sending SMS. This number will be used as the
FROM
number. - Copy this Vonage Virtual Number into the
VONAGE_FROM_NUMBER
field in your.env
file (use E.164 format, e.g.,14155550100
).
Step 4: Set up ngrok
for Local Webhook Testing
Vonage needs a publicly accessible URL to send webhooks. ngrok
creates a secure tunnel to your local machine.
- If your server is running, stop it (Ctrl+C).
- Open a terminal in your project directory and run
ngrok
(replace3000
if your app uses a different port):ngrok http 3000
ngrok
will display forwarding URLs (e.g.,https://random-subdomain.ngrok-free.app
). Copy thehttps
URL. This isYOUR_NGROK_URL
.- Update Vonage Application: Go back to your Application settings in the Vonage dashboard. Edit the Messages capability URLs:
- Inbound URL:
YOUR_NGROK_URL/webhooks/inbound
- Status URL:
YOUR_NGROK_URL/webhooks/status
- Save the changes.
- Inbound URL:
- Update
.env
: SetBASE_URL=YOUR_NGROK_URL
in your.env
file. This helps keep track of the currently activengrok
URL. - Restart your Node.js server:
node index.js
Step 5: Implement Webhook Handlers
Add routes in index.js
to receive POST requests from Vonage at the URLs you configured.
// index.js (add these route handlers)
// --- Webhook Handler for Message Status Updates ---
app.post('/webhooks/status', (req, res) => {
const statusData = req.body;
console.log('Received Status Webhook:', JSON.stringify(statusData, null, 2));
// --- Process the status update ---
// Examples: Update a database record, log specific statuses, trigger alerts
try {
if (statusData.status === 'delivered') {
console.log(`Message ${statusData.message_uuid} delivered to ${statusData.to} at ${statusData.timestamp}`);
} else if (statusData.status === 'failed' || statusData.status === 'rejected') {
// Extract error details carefully, structure might vary slightly
const reason = statusData.error?.reason || statusData.error?.title || 'Unknown';
const code = statusData.error?.code || 'N/A';
console.error(`Message ${statusData.message_uuid} failed for ${statusData.to}. Reason: ${reason} (Code: ${code})`);
// Log full error object if needed for debugging complex failures
// console.error('Full error object:', statusData.error);
} else {
console.log(`Message ${statusData.message_uuid} status update: ${statusData.status}`);
}
} catch (error) {
console.error('Error processing status webhook:', error);
// Decide if you still want to send 200 OK even if processing failed internally
}
// --- IMPORTANT: Respond with 200 OK ---
// Vonage expects a 200 OK response to acknowledge receipt of the webhook.
// Failure to do so will result in retries from Vonage.
res.status(200).send('OK');
});
// --- Webhook Handler for Inbound Messages (Optional) ---
app.post('/webhooks/inbound', (req, res) => {
const inboundData = req.body;
console.log('Received Inbound SMS:', JSON.stringify(inboundData, null, 2));
// --- Process inbound message ---
// Example: Log message, trigger auto-reply, etc.
try {
console.log(`SMS received from ${inboundData.from} with text: ""${inboundData.text}""`);
// Add your inbound message processing logic here
} catch (error) {
console.error('Error processing inbound webhook:', error);
}
// Respond with 200 OK
res.status(200).send('OK');
});
/webhooks/status
: Logs the received status data. You would typically parse this data (message_uuid
,status
,error
,timestamp
) to update the delivery status of your sent messages, perhaps in a database. Error field extraction is made slightly more robust. Added basictry...catch
./webhooks/inbound
: Logs incoming messages sent to your Vonage number. Added basictry...catch
.res.status(200).send('OK')
: Crucial! Vonage needs this acknowledgment.
Verification: Send a test broadcast using the /broadcast
endpoint again. Watch your server logs. You should see:
- Logs from
/broadcast
initiating the sends. - Logs from
/webhooks/status
showing updates likesubmitted
,delivered
, orfailed
for each message sent (these may arrive with some delay).
5. Implementing Error Handling, Logging, and Retry Mechanisms
Robust applications need solid error handling and logging.
Error Handling:
- SDK Errors: The
try...catch
block in thesendSms
function (updated in Section 2) handles errors during the API call. It now attempts to log more detailed error information. - Webhook Errors: Wrap the logic inside webhook handlers in
try...catch
(as shown in Section 4) to prevent the server from crashing if processing fails. Ensure you still send200 OK
unless it's a fundamental issue receiving the request. - Input Validation: The
/broadcast
endpoint includes basic validation. Enhance this using libraries likejoi
orexpress-validator
, and especiallylibphonenumber-js
for phone numbers.
Logging:
- Consistency: Use
console.log
,console.warn
,console.error
consistently. The enhancedsendSms
provides better context. - Information: Log key events: server start, request received, message submission attempts (success/failure with details), webhook received, specific statuses (delivered/failed). Include relevant data like
message_uuid
, recipient number, timestamps, and error details. - Production Logging: For production, consider using a dedicated logging library like
winston
orpino
for structured logging (JSON), log levels, and transports (files, external services).
Retry Mechanisms:
- Vonage Webhooks: Vonage automatically retries sending webhooks if it doesn't receive a
200 OK
response. Ensure your webhook handlers are reliable and respond quickly. - Sending SMS: If
sendSms
fails due to potentially temporary issues (e.g., network timeout, rate limiting error429
), you might implement a limited retry strategy.- Simple Retry: Wrap the
sendSms
call in a loop with delays. - Exponential Backoff: Increase the delay between retries (e.g., 1s, 2s, 4s). Libraries like
async-retry
can help.
- Simple Retry: Wrap the
Example: Simple Retry Logic (Conceptual - Integrate into /broadcast
loop)
This shows how you might add retry logic within the /broadcast
handler's loop.
// Conceptual - Integrating into the /broadcast loop
// Requires careful implementation to avoid infinite loops and manage delays
const MAX_RETRIES = 2;
const RETRY_DELAY_MS = 2000; // Initial delay for first retry
for (const recipient of recipients) {
let attempt = 0;
let result;
while (attempt <= MAX_RETRIES) {
result = await sendSms(recipient_ message);
// Check if successful OR if the error is NOT retryable
if (result.success || !isRetryableError(result.errorCode)) {
break; // Success or non-retryable error_ stop retrying
}
attempt++;
if (attempt <= MAX_RETRIES) {
const delay = RETRY_DELAY_MS * Math.pow(2_ attempt - 1); // Exponential backoff
console.warn(`[SMS to ${recipient}] Retrying (${attempt}/${MAX_RETRIES}) after ${delay}ms delay due to error code ${result.errorCode}...`);
await sleep(delay);
} else {
console.error(`[SMS to ${recipient}] Max retries reached. Giving up after error code ${result.errorCode}.`);
}
}
results.push(result); // Add final result (success or last failure)
// ... rest of loop ...
// Main delay between *different* recipients (still subject to Section 2 caveats)
if (attempt <= MAX_RETRIES) { // Only delay if we didn't just finish retrying
await sleep(SEND_DELAY_MS);
}
}
function isRetryableError(errorCode) {
// Example: Vonage uses 429 for rate limits_ 5xx for server issues
// Check if errorCode is a string representing a number before parsing
const numericCode = parseInt(errorCode_ 10);
if (isNaN(numericCode)) {
return false; // Cannot retry non-numeric error codes this way
}
const retryableCodes = [429_ 500_ 502_ 503_ 504]; // Add other transient codes if identified
return retryableCodes.includes(numericCode);
}
- Caution: Implement retries carefully. Only retry transient issues (like
429
_5xx
). Retrying permanent errors (400
bad number) is wasteful. Ensure delays don't compound excessively. For large scale_ offload sending to a background queue system (like BullMQ) which handles retries more robustly. TheisRetryableError
function now handles potentially non-numericerrorCode
values.
6. Creating a Database Schema and Data Layer (Optional but Recommended)
For tracking broadcast jobs_ individual message statuses_ and managing recipients effectively_ a database is highly recommended. Here's a conceptual example using Prisma (a popular Node.js ORM).
Step 1: Install Prisma
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider postgresql # Or sqlite_ mysql_ etc.
This creates a prisma
folder with schema.prisma
and updates .env
with DATABASE_URL
.
Step 2: Define Schema (prisma/schema.prisma
)
// This is your Prisma schema file_
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = ""prisma-client-js""
}
datasource db {
provider = ""postgresql"" // Or your chosen provider
url = env(""DATABASE_URL"")
}
model Broadcast {
id String @id @default(cuid())
message String
status String @default(""PENDING"") // PENDING_ PROCESSING_ COMPLETED_ FAILED
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
recipients Recipient[]
}
model Recipient {
id String @id @default(cuid())
phoneNumber String
status String @default(""PENDING"") // PENDING_ QUEUED_ SUBMITTED_ DELIVERED_ FAILED_ REJECTED
vonageMessageUuid String? @unique // Link to Vonage message
submittedAt DateTime?
lastStatusUpdate DateTime?
errorMessage String?
errorCode String? // Store error code from submission or webhook
broadcast Broadcast @relation(fields: [broadcastId]_ references: [id])
broadcastId String
@@index([broadcastId])
@@index([status])
@@index([vonageMessageUuid]) // Index for webhook lookup
}
Broadcast
: Represents a single bulk sending job.Recipient
: Represents each individual message within a broadcast_ tracking its specific status and VonagemessageUuid
. Added index onvonageMessageUuid
.- Relations: A one-to-many relationship between
Broadcast
andRecipient
. - Indexes: Added for potentially common queries.
Step 3: Apply Schema Changes
Create and apply the first migration.
npx prisma migrate dev --name init
Step 4: Integrate Prisma Client
Instantiate the client and use it to interact with the database. Handle potential connection errors and ensure graceful shutdown.
// index.js (add near the top)
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Basic check for DB connection on startup (optional but good)
async function checkDbConnection() {
try {
await prisma.$connect();
console.log('Database connection successful.');
} catch (error) {
console.error('Database connection failed:'_ error);
process.exit(1); // Exit if DB is essential
}
}
checkDbConnection(); // Call the check
// Graceful shutdown for Prisma
async function shutdown() {
console.log('Disconnecting database...');
try {
await prisma.$disconnect();
console.log('Database disconnected.');
} catch (e) {
console.error('Error disconnecting database:'_ e);
} finally {
server.close(() => { // Close HTTP server after DB disconnect attempt
console.log('Server closed.');
process.exit(0);
});
// Force exit after timeout if server doesn't close gracefully
setTimeout(() => {
console.error('Forcefully shutting down after timeout.');
process.exit(1);
}, 5000); // 5 second timeout
}
}
process.on('SIGTERM', shutdown); // Handle termination signal
process.on('SIGINT', shutdown); // Handle interrupt signal (Ctrl+C)
- Added basic connection check and graceful shutdown handlers.
Step 5: Modify API and Webhooks to Use Database
- /broadcast Endpoint:
- Create a
Broadcast
record. - Create
Recipient
records (statusPENDING
orQUEUED
). - Respond
202 Accepted
immediately after creating records to indicate processing will happen asynchronously. - Then, iterate through recipients (or preferably, trigger background jobs).
- When
sendSms
is called and returns successfully, update the correspondingRecipient
record (status: 'SUBMITTED'
,vonageMessageUuid
,submittedAt
). - If
sendSms
fails, update theRecipient
record (status: 'FAILED'
,errorMessage
,errorCode
).
- Create a
- /webhooks/status Handler:
- Find the
Recipient
record using thevonageMessageUuid
from the webhook data. - Update the
Recipient
'sstatus
,lastStatusUpdate
, and potentiallyerrorMessage
/errorCode
based on the webhook content (delivered
,failed
,rejected
, etc.). - Handle cases where the
messageUuid
might not be found (e.g., log an error).
- Find the
(Implementing these database interactions is left as an exercise but involves using prisma.broadcast.create
, prisma.recipient.createMany
, prisma.recipient.update
, and prisma.recipient.findUnique
within the respective route handlers.)