code examples
code examples
Schedule SMS Messages with Vonage Messages API in Node.js [2025 Guide]
Learn how to schedule SMS reminders using Vonage Messages API with Node.js and Express. Complete tutorial covering setTimeout scheduling, database persistence with SQLite, API authentication with Application IDs, and production deployment for automated messaging systems.
Build an SMS Scheduling and Reminder Service with Vonage
Build a production-ready SMS scheduling and reminder service using Node.js, Express, and the Vonage Messages API. Learn how to schedule SMS reminders to be sent at specific future times, manage scheduled messages via a REST API, handle the setTimeout maximum delay constraint, and implement database persistence with SQLite. Master authentication with Application IDs and private keys, error handling, and production deployment strategies for reliable automated messaging.
Solve the common need to notify users via SMS about appointments, deadlines, or other time-sensitive events without requiring manual intervention at the time of sending.
Technologies used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express: A minimal and flexible Node.js web application framework for building the API.
- Vonage Messages API: Enables sending SMS (and other message types) programmatically.
node-cron: A simple cron-like job scheduler for Node.js (potentially useful for periodic tasks like cleanup, though this guide primarily usessetTimeoutfor direct scheduling).dotenv: Loads environment variables from a.envfile.uuid: Generates unique identifiers for reminders.- (Optional but Recommended) SQLite: A simple file-based database for persistent storage of reminders.
System architecture:
+---------+ +-----------------+ +-----------------+ +--------+ +------+
| User | ----> | Express API | ----> | Reminder Logic | ----> | Vonage | ----> | SMS |
| (Admin) | | (POST /schedule)| | (setTimeout/DB) | | API | | User |
+---------+ +-----------------+ +-----------------+ +--------+ +------+
| ^ |
| GET /reminders/:id | |
| DELETE /reminders/:id| |
+---------------------+----------------------------------------------+Final outcome:
By the end of this guide, you will have a Node.js application with API endpoints to:
- Schedule an SMS reminder for a specific recipient, message, and future time.
- (Optional) List scheduled reminders.
- (Optional) Cancel a pending reminder.
- Securely store credentials and handle errors gracefully.
- (Optional) Persist reminder data in a database.
Prerequisites:
- Node.js and npm (or yarn): Installed on your system. Download Node.js
- Recommended: Node.js v20 LTS (Iron) or v22 LTS (Jod) as of 2025. Reference: Node.js Release Schedule
- Vonage API Account: Sign up for free at Vonage.
- Vonage API Key and Secret: Found on your Vonage API Dashboard.
- Vonage Application: You'll create one to get an Application ID and Private Key.
- Vonage Virtual Number: Purchase one from the Vonage Dashboard (Numbers > Buy Numbers) capable of sending SMS. Link this number to your Vonage Application.
- (Optional for Local Development)
ngrok: Useful for exposing your local server to the internet, primarily for receiving webhook callbacks from Vonage (like delivery receipts or inbound messages), which are not covered in the basic implementation of this guide but mentioned as potential extensions. It's not required just for scheduling and sending outbound SMS. Get ngrok
1. Set Up Your Node.js SMS Scheduler Project
Initialize the Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
bashmkdir vonage-sms-scheduler cd vonage-sms-scheduler -
Initialize npm Project: This creates a
package.jsonfile.bashnpm init -y -
Install Dependencies: We need Express for the server, the Vonage SDK,
dotenvfor environment variables,uuidfor unique IDs, andnode-cron(though optional for this guide's core logic).bashnpm install express @vonage/server-sdk dotenv node-cron uuid(Optional: For database persistence)
bashnpm install sqlite3(Optional: For robust input validation)
bashnpm install express-validator -
Create Project Structure: Create the basic files and directories.
bashtouch server.js .env .gitignore mkdir data # Optional: For SQLite database file mkdir src touch src/vonageClient.js src/scheduler.js mkdir src/routes touch src/routes/reminderRoutes.js # Optional: For DB touch src/database.jsYour structure should look something like this:
textvonage-sms-scheduler/ ├── data/ ├── node_modules/ ├── src/ │ ├── database.js # Optional DB logic │ ├── scheduler.js # Scheduling logic │ ├── vonageClient.js # Vonage SDK setup │ └── routes/ │ └── reminderRoutes.js # API routes ├── .env # Environment variables (API keys, etc.) ├── .gitignore # Files/folders to ignore in Git ├── package.json ├── package-lock.json └── server.js # Main Express server setup -
Configure
.gitignore: Prevent sensitive information and unnecessary files from being committed to version control. Add the following to your.gitignorefile:text# Dependencies node_modules/ # Environment variables .env # Optional database file data/*.sqlite # Logs *.log private.key # Example if storing key in root # OS generated files .DS_Store Thumbs.db -
Set Up Vonage Application and Credentials:
- Go to your Vonage API Dashboard.
- Click "Create a new application".
- Give it a name (e.g., "Node SMS Scheduler").
- Click "Generate public and private key". Save the
private.keyfile that downloads. For simplicity in this guide, you can save it directly into your project's root directory (vonage-sms-scheduler/private.key), ensuring.gitignoreincludesprivate.keyor*.key. Security Warning: Storing private keys directly in the project directory, even if gitignored, is generally discouraged for production environments. Consider storing it outside the project root, using environment variables to pass the key content (not the path), or leveraging secrets management services. - Enable the "Messages" capability. For sending scheduled messages, you don't strictly need to configure Inbound/Status URLs immediately, but if you plan to handle replies or delivery receipts later, set them up using an
ngrokURL (e.g.,https://<your-ngrok-url>.ngrok.io/webhooks/inboundand/status). - Scroll down to "Link virtual numbers" and link the Vonage number you purchased earlier to this application.
- Click "Create application".
- Note down the Application ID.
-
Configure Environment Variables (
.env): Create a.envfile in the project root and add your Vonage credentials and configuration. Never commit this file to Git.dotenv# .env # Vonage Credentials # Found in Vonage Dashboard > API Settings VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET # Vonage Application Credentials # From the application you created VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID # Path relative to the project root where you saved the private key VONAGE_PRIVATE_KEY_PATH=./private.key # Vonage Number associated with your application VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # e.g., 14155550100 # Server Configuration PORT=3000 # Optional: Database path DATABASE_PATH=./data/reminders.sqlite # Optional: Simple API Key for endpoint protection # INTERNAL_API_KEY=your-secret-api-keyVONAGE_API_KEY,VONAGE_API_SECRET: Get from the main Vonage Dashboard API settings page.VONAGE_APPLICATION_ID: Get from the specific Vonage Application page you created.VONAGE_PRIVATE_KEY_PATH: The path to theprivate.keyfile you downloaded when creating the application.VONAGE_NUMBER: The Vonage virtual number you linked to the application, in E.164 format (e.g., 12015550123).PORT: The port your Express server will listen on.DATABASE_PATH: Location for the SQLite database file (if using).INTERNAL_API_KEY: (Optional) A secret key you can define to protect your API endpoints.
2. Configure Vonage Messages API Authentication in Node.js
Configure the Vonage Node.js SDK to interact with the API using the credentials from your .env file.
File: src/vonageClient.js
// src/vonageClient.js
const { Vonage } = require('@vonage/server-sdk');
const { Auth } = require('@vonage/auth');
const fs = require('fs'); // Needed to check private key path
require('dotenv').config(); // Load environment variables
// --- Input Validation ---
// Basic check to ensure required environment variables are set
const requiredEnvVars = [
'VONAGE_API_KEY',
'VONAGE_API_SECRET',
'VONAGE_APPLICATION_ID',
'VONAGE_PRIVATE_KEY_PATH',
'VONAGE_NUMBER'
];
let hasError = false;
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
console.error(`Error: Missing required environment variable ${envVar}. Please check your .env file.`);
hasError = true;
}
}
// Check if private key file exists
if (process.env.VONAGE_PRIVATE_KEY_PATH && !fs.existsSync(process.env.VONAGE_PRIVATE_KEY_PATH)) {
console.error(`Error: Private key file not found at path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`);
hasError = true;
}
if (hasError) {
process.exit(1); // Exit if critical config is missing or invalid
}
// --- Vonage Authentication ---
// Use Application ID and Private Key for authentication with Messages API
// The Vonage SDK requires API Key/Secret for basic instantiation, even if subsequent calls
// like Messages API primarily use Application ID/Private Key for JWT generation.
const credentials = new Auth({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
});
// --- Vonage Client Initialization ---
const vonage = new Vonage(credentials);
// --- SMS Sending Function ---
/**
* Sends an SMS message using the Vonage Messages API.
* @param {string} to - The recipient phone number (E.164 format).
* @param {string} text - The message content.
* @returns {Promise<string>} - Resolves with the message UUID on success.
* @throws {Error} - Rejects with an error if sending fails.
*/
async function sendSms(to, text) {
const fromNumber = process.env.VONAGE_NUMBER;
console.log(`Attempting to send SMS from ${fromNumber} to ${to}`);
try {
const resp = await vonage.messages.send({
message_type: "text",
text: text,
to: to,
from: fromNumber,
channel: "sms"
});
console.log(`SMS sent successfully to ${to}. Message UUID: ${resp.message_uuid}`);
return resp.message_uuid;
} catch (err) {
console.error(`Error sending SMS to ${to}:`, err.response ? err.response.data : err.message);
// Rethrow a more specific error or handle as needed
throw new Error(`Failed to send SMS via Vonage: ${err.message}`);
}
}
module.exports = {
sendSms,
vonage
};Explanation:
- Load Env Vars:
dotenv.config()loads variables from.env. - Validation: Includes checks for crucial Vonage environment variables and verifies the existence of the private key file.
- Authentication: We use the
Authobject withapplicationIdandprivateKeyfor the Messages API, as recommended. The SDK also requiresapiKeyandapiSecretfor initialization. - Client Initialization: Creates the
vonageinstance. sendSmsFunction:- Takes the recipient
tonumber andtextmessage. - Retrieves the
VONAGE_NUMBERfrom environment variables. - Uses
vonage.messages.send()specifyingchannel: "sms",message_type: "text". - Includes logging for success and detailed error logging, accessing
err.response.dataif available. - Returns the
message_uuidon success or throws an error on failure.
- Takes the recipient
3. Build SMS Scheduling Logic with setTimeout in Node.js
Use Node.js's built-in setTimeout function to schedule the SMS sending for a specific future time. Manage pending timers in memory initially. Note: This approach is simple but not robust against server restarts; scheduled reminders will be lost. See Section 5 for database persistence.
File: src/scheduler.js
// src/scheduler.js
const { sendSms } = require('./vonageClient');
const { v4: uuidv4 } = require('uuid'); // Use uuid for unique IDs
// --- In-Memory Storage for Pending Reminders ---
// WARNING: This is NOT persistent. If the server restarts, pending reminders are lost.
// A database (see Section 5) is strongly recommended for reliability.
const pendingReminders = new Map(); // Map<reminderId, { timeoutId: NodeJS.Timeout, recipient: string, message: string, sendAt: Date, status: string }>
/**
* Schedules an SMS reminder using in-memory storage.
* @param {string} recipient - Recipient phone number (E.164 format).
* @param {string} message - Message content.
* @param {Date} sendAtDate - The Date object representing when to send the message.
* @returns {string} The unique ID of the scheduled reminder.
* @throws {Error} If scheduling fails (e.g., time is in the past).
*/
function scheduleReminder(recipient, message, sendAtDate) {
const now = new Date();
const delay = sendAtDate.getTime() - now.getTime();
if (delay <= 0) {
throw new Error("Scheduled time must be in the future.");
}
// Limit delay to avoid issues with extremely long timeouts (optional)
// const maxDelay = 2147483647; // Max 32-bit integer (~24.8 days)
// if (delay > maxDelay) {
// throw new Error(`Scheduling delay too long (max ~24.8 days). Consider using a persistent job queue for longer schedules.`);
// }
const reminderId = uuidv4(); // Generate a unique ID
console.log(`Scheduling reminder ${reminderId} for ${recipient} at ${sendAtDate.toISOString()} (in ${delay}ms)`);
const timeoutId = setTimeout(async () => {
console.log(`Executing reminder ${reminderId} for ${recipient}`);
const reminder = pendingReminders.get(reminderId); // Get latest state
if (!reminder || reminder.status !== 'pending') {
console.warn(`Reminder ${reminderId} execution skipped: No longer pending (status: ${reminder?.status})`);
pendingReminders.delete(reminderId); // Clean up if somehow still present
return;
}
try {
const messageUuid = await sendSms(recipient, message);
console.log(`Reminder ${reminderId} sent successfully. Vonage Message ID: ${messageUuid}`);
// Update status in memory
pendingReminders.set(reminderId, { ...reminder, status: 'sent', vonageMessageId: messageUuid });
// Optionally update status in DB here if using persistence
} catch (error) {
console.error(`Failed to send reminder ${reminderId}:`, error);
// Update status in memory
pendingReminders.set(reminderId, { ...reminder, status: 'failed' });
// Optionally update status in DB here (e.g., 'failed')
} finally {
// Remove from active map *after* processing (sent or failed) for status query purposes
// Or keep it with final status, depending on requirements for getReminderStatus
// For this simple version, let's keep the final status available for a while.
// A cleanup mechanism might be needed for long-running servers.
console.log(`Reminder ${reminderId} processed. Final status: ${pendingReminders.get(reminderId)?.status}`);
}
}, delay);
// Store reminder details and the timeout handle
pendingReminders.set(reminderId, {
timeoutId,
recipient,
message,
sendAt: sendAtDate,
status: 'pending' // Initial status
});
return reminderId;
}
/**
* Cancels a pending reminder (in-memory version).
* @param {string} reminderId - The ID of the reminder to cancel.
* @returns {boolean} True if cancelled successfully, false otherwise (e.g., not found or already processed).
*/
function cancelReminder(reminderId) {
const reminder = pendingReminders.get(reminderId);
if (reminder && reminder.status === 'pending') {
clearTimeout(reminder.timeoutId); // Clear the scheduled timeout
// Update status in memory
pendingReminders.set(reminderId, { ...reminder, status: 'cancelled' });
console.log(`Cancelled reminder ${reminderId}`);
// Optionally update status in DB here
return true;
}
console.log(`Reminder ${reminderId} not found or cannot be cancelled (status: ${reminder?.status}).`);
return false;
}
/**
* Gets the details of a reminder (pending, sent, failed, cancelled) from memory.
* @param {string} reminderId - The ID of the reminder.
* @returns {object | undefined} Reminder details (excluding timeoutId) or undefined if not found.
*/
function getReminderStatus(reminderId) {
const reminder = pendingReminders.get(reminderId);
if(reminder) {
// Return a copy without the internal timeoutId
const { timeoutId, ...details } = reminder;
return details;
}
return undefined;
}
module.exports = {
scheduleReminder,
cancelReminder,
getReminderStatus,
};Explanation:
pendingRemindersMap: An in-memoryMapstores reminder states. The key isreminderId, value includestimeoutId, details, andstatus. Data is volatile.scheduleReminder:- Calculates
delay. Checks if it's in the future. - Generates
reminderIdusinguuid. - Uses
setTimeout. The callback checks the reminder status before sending (in case it was cancelled), callssendSms, and updates the in-memory status to 'sent' or 'failed'. - Stores initial details and
timeoutIdinpendingReminderswith 'pending' status. - Returns
reminderId.
- Calculates
cancelReminder:- Looks up the reminder. Checks if its status is 'pending'.
- If yes, calls
clearTimeoutand updates the in-memory status to 'cancelled'. - Returns
trueif cancelled,falseotherwise.
getReminderStatus:- Retrieves reminder details from the map (excluding
timeoutId). Returns the current state ('pending', 'sent', 'failed', 'cancelled') if found.
- Retrieves reminder details from the map (excluding
4. Create Express API Endpoints for SMS Scheduling
Create the Express server and define API endpoints to interact with your scheduler.
File: src/routes/reminderRoutes.js
// src/routes/reminderRoutes.js
const express = require('express');
const { scheduleReminder, cancelReminder, getReminderStatus } = require('../scheduler');
// Optional: Input validation library
// const { body, param, validationResult } = require('express-validator');
const router = express.Router();
// --- Middleware for Basic Input Validation (Example) ---
// For production applications, using a dedicated validation library like express-validator
// is highly recommended for more robust and maintainable validation.
// This example shows basic manual validation.
const validateScheduleRequest = (req, res, next) => {
const { recipient, message, sendAt } = req.body;
if (!recipient || !message || !sendAt) {
return res.status(400).json({ error: 'Missing required fields: recipient, message, sendAt' });
}
// E.164 phone number format validation (ITU-T specification)
// Must start with +, followed by 1-15 digits, first digit cannot be 0
// Reference: https://www.itu.int/rec/T-REC-E.164
if (!/^\+[1-9]\d{1,14}$/.test(recipient)) {
return res.status(400).json({ error: 'Invalid recipient phone number format (must be E.164: + followed by country code and number, 1-15 digits total, e.g., +14155550100)' });
}
// Check if sendAt is a valid date string parseable by new Date()
const sendAtDate = new Date(sendAt);
if (isNaN(sendAtDate.getTime())) {
// More specific check for ISO 8601 format might be desired
return res.status(400).json({ error: 'Invalid sendAt format. Use a valid date string (ISO 8601 recommended, e.g., 2025-12-31T23:59:59.000Z)' });
}
// Attach parsed date to request object for handler use
req.sendAtDate = sendAtDate;
next();
};
// --- API Endpoints ---
// POST /api/reminders - Schedule a new reminder
router.post('/', validateScheduleRequest, (req, res, next) => {
const { recipient, message } = req.body;
const { sendAtDate } = req; // Get parsed date from validation middleware
try {
const reminderId = scheduleReminder(recipient, message, sendAtDate);
res.status(202).json({
message: "Reminder scheduled successfully.",
reminderId: reminderId,
details: {
recipient: recipient,
message: message,
sendAt: sendAtDate.toISOString() // Return consistent ISO string format
}
});
} catch (error) {
// Handle specific scheduling errors like "time in the past"
if (error.message.includes("future")) {
return res.status(400).json({ error: error.message });
}
// Pass other errors (e.g., from uuid, setTimeout setup) to the central error handler
next(error);
}
});
// GET /api/reminders/:id - Get status of a specific reminder
router.get('/:id', async (req, res, next) => { // Mark as async if DB check is added
const { id } = req.params;
try {
const reminder = getReminderStatus(id); // Check in-memory store first
if (reminder) {
res.status(200).json(reminder);
} else {
// Optional: If using DB persistence, check the database here
// const dbReminder = await findReminderInDb(id); // Assuming findReminderInDb exists
// if (dbReminder) {
// // Map DB fields to the expected response structure if needed
// return res.status(200).json(dbReminder);
// }
res.status(404).json({ error: `Reminder with ID ${id} not found.` });
}
} catch (error) {
next(error); // Pass DB errors etc. to central handler
}
});
// DELETE /api/reminders/:id - Cancel a pending reminder
router.delete('/:id', (req, res, next) => {
const { id } = req.params;
try {
const cancelled = cancelReminder(id);
if (cancelled) {
res.status(200).json({ message: `Reminder ${id} cancelled successfully.` });
} else {
// Could be not found, or already sent/failed/cancelled
// Check current status to provide a more specific message if needed
const currentStatus = getReminderStatus(id)?.status;
if (currentStatus) {
res.status(400).json({ error: `Reminder ${id} cannot be cancelled (current status: ${currentStatus}).` });
} else {
res.status(404).json({ error: `Reminder with ID ${id} not found.` });
}
// Or simply keep the 404 for "not found or cannot be cancelled now"
// res.status(404).json({ error: `Reminder with ID ${id} not found or cannot be cancelled.` });
}
} catch (error) {
next(error); // Pass errors to central handler
}
});
module.exports = router;File: server.js (Main application setup)
// server.js
require('dotenv').config(); // Ensure env vars are loaded first
const express = require('express');
const reminderRoutes = require('./src/routes/reminderRoutes');
// Optional: Database initialization and rescheduling logic
// const { initializeDatabase, getPendingRemindersFromDb } = require('./src/database');
// const { loadReminderIntoMemory } = require('./src/scheduler'); // Need this if rescheduling from DB
const app = express();
const PORT = process.env.PORT || 3000;
// --- Middleware ---
app.use(express.json()); // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// --- Basic Logging Middleware ---
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// --- Optional Simple API Key Auth Middleware ---
// Uncomment and configure INTERNAL_API_KEY in .env to use
/*
const API_KEY = process.env.INTERNAL_API_KEY;
function authenticateKey(req, res, next) {
const providedKey = req.headers['x-api-key'];
if (!API_KEY) {
console.warn("Warning: INTERNAL_API_KEY not set, skipping API authentication.");
return next(); // Skip auth if no key configured (for dev/testing)
}
if (providedKey && providedKey === API_KEY) {
next(); // Key is valid
} else {
res.status(401).json({ error: 'Unauthorized: Invalid or missing API Key in X-API-Key header' });
}
}
// Apply authentication to reminder routes
app.use('/api/reminders', authenticateKey, reminderRoutes);
*/
// --- Routes ---
// Apply reminder routes (without auth middleware if commented out above)
app.use('/api/reminders', reminderRoutes);
app.get('/', (req, res) => {
res.send('SMS Reminder Service is running!');
});
// --- 404 Handler ---
// Handles requests that don't match any routes defined above
app.use((req, res, next) => {
res.status(404).json({ error: 'Not Found' });
});
// --- Central Error Handler ---
// Must be defined last, after all routes and other middleware.
// Catches errors passed via next(error).
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
console.error("Unhandled Error:", err.stack || err.message); // Log the full error stack
// Respond with a generic 500 error in production
// Avoid leaking detailed error information
res.status(err.status || 500).json({ // Use error status if available, otherwise 500
error: 'Internal Server Error',
// message: process.env.NODE_ENV === 'development' ? err.message : undefined // Optionally show details in dev
});
});
// --- Start Server ---
async function startServer() {
try {
// Optional: Initialize Database and Reschedule Tasks
/*
await initializeDatabase();
console.log("Database initialized successfully.");
// Reschedule pending tasks from DB on startup
console.log("Checking for pending reminders in DB to reschedule...");
const pendingDbReminders = await getPendingRemindersFromDb(); // Fetch pending tasks
let rescheduledCount = 0;
for (const reminder of pendingDbReminders) {
try {
const sendAtDate = new Date(reminder.sendAt);
if (sendAtDate > new Date()) { // Only reschedule if time is still in the future
console.log(`Rescheduling reminder from DB: ${reminder.id} for ${sendAtDate.toISOString()}`);
// --- Requires modification in scheduler.js ---
// Need a function like 'loadReminderIntoMemory' that creates the timeout
// based on DB data without re-inserting into DB, and populates an
// in-memory map of active timeouts (e.g., activeTimeouts<id, timeoutId>).
loadReminderIntoMemory(reminder.id, reminder.recipient, reminder.message, sendAtDate);
rescheduledCount++;
} else {
console.warn(`Reminder ${reminder.id} from DB has past send time (${reminder.sendAt}), marking as missed.`);
// Optionally update DB status to 'missed' or 'failed_startup'
// await updateReminderStatusInDb(reminder.id, 'missed');
}
} catch (error) {
console.error(`Failed to reschedule reminder ${reminder.id} from DB:`, error);
}
}
console.log(`Rescheduled ${rescheduledCount} pending reminders from the database.`);
*/
// Start listening for HTTP requests
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`API endpoints available at http://localhost:${PORT}/api/reminders`);
console.log(`Current server time: ${new Date().toISOString()} (Ensure server timezone is correct, preferably UTC)`);
});
} catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
}
startServer();Explanation:
reminderRoutes.js:- Defines an Express
Router. - Includes basic input validation middleware (
validateScheduleRequest). Usingexpress-validatoris recommended for production. - POST
/: Schedules a reminder viascheduleReminder. Returns202 AcceptedwithreminderId. Handles specific scheduling errors. - GET
/:id: Retrieves status viagetReminderStatus. Returns404if not found. Includes comments for optional DB lookup integration. - DELETE
/:id: Cancels viacancelReminder. Returns200on success,404or400if not found or not cancellable.
- Defines an Express
server.js:- Sets up the Express app, loads
dotenv. - Uses
express.json()andexpress.urlencoded(). - Includes simple request logging.
- Includes commented-out example for simple API key authentication.
- Mounts
reminderRoutesunder/api/reminders. - Defines 404 and central error handling middleware (crucial for catching unexpected errors).
- Contains a
startServerfunction with commented-out logic for database initialization and rescheduling on startup (requires Section 5 implementation). - Starts the server and logs relevant information.
- Sets up the Express app, loads
5. (Optional but Recommended) Implement Database Persistence for SMS Reminders
Use a database like SQLite to provide persistence. The in-memory map (pendingReminders) is simple but not reliable; data is lost on restart.
File: src/database.js
// src/database.js
const sqlite3 = require('sqlite3').verbose(); // Use verbose for detailed logs during dev
require('dotenv').config();
const dbPath = process.env.DATABASE_PATH || './data/reminders.db'; // Default path
// --- Database Connection ---
// Creates the directory and file if they don't exist
const dbDir = require('path').dirname(dbPath);
if (!require('fs').existsSync(dbDir)) {
require('fs').mkdirSync(dbDir, { recursive: true });
}
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error("Error connecting to SQLite database:", err.message);
throw err; // Fail fast if DB connection fails
}
console.log(`Connected to SQLite database at ${dbPath}`);
});
// --- Database Initialization (Schema Setup) ---
/**
* Creates the reminders table if it doesn't exist.
* Should be called once on server startup.
*/
function initializeDatabase() {
return new Promise((resolve, reject) => {
const sql = `
CREATE TABLE IF NOT EXISTS reminders (
id TEXT PRIMARY KEY, -- Unique reminder ID (UUID)
recipient TEXT NOT NULL, -- E.164 phone number
message TEXT NOT NULL,
sendAt TEXT NOT NULL, -- ISO 8601 timestamp string (UTC recommended)
status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'sent', 'failed', 'cancelled', 'missed'
createdAt TEXT NOT NULL, -- ISO 8601 timestamp string
updatedAt TEXT NOT NULL, -- ISO 8601 timestamp string
vonageMessageId TEXT -- Store Vonage message UUID after sending
);
`;
db.run(sql, (err) => {
if (err) {
console.error("Error creating reminders table:", err.message);
return reject(err);
}
console.log("Reminders table checked/created successfully.");
resolve();
});
});
}
// --- Database Operations ---
module.exports = {
initializeDatabase,
db
};Explanation:
- Database Path: Uses the
DATABASE_PATHenvironment variable or defaults to./data/reminders.db. - Directory Creation: Automatically creates the database directory if it doesn't exist.
- Connection: Establishes a SQLite connection on module load.
- Schema Initialization: The
initializeDatabase()function creates thereminderstable with appropriate columns for tracking reminder lifecycle. - Status Field: Supports multiple states: 'pending', 'sent', 'failed', 'cancelled', 'missed'.
Frequently Asked Questions About SMS Scheduling with Vonage
How Do I Schedule SMS Messages with Vonage Messages API in Node.js?
Schedule SMS messages by using Node.js's setTimeout function combined with the Vonage Messages API. Create an Express API endpoint that accepts a recipient phone number (in E.164 format), message text, and future timestamp. Calculate the delay in milliseconds, schedule a timeout, and store the reminder details in memory or a database. When the timeout executes, call vonage.messages.send() to deliver the SMS. This approach works for schedules up to 24.8 days in advance due to setTimeout's 32-bit integer limit.
What Is the Maximum Delay for setTimeout When Scheduling SMS Reminders?
The maximum delay for setTimeout is 2,147,483,647 milliseconds (approximately 24.8 days) due to JavaScript's 32-bit signed integer storage limitation. For schedules beyond this limit, use a persistent job queue like Bull, Agenda, or BullMQ. These libraries support longer-term scheduling by storing jobs in Redis or MongoDB and checking periodically for due tasks. Reference: MDN setTimeout Maximum Delay
How Do I Authenticate with Vonage Messages API for SMS Scheduling?
Authenticate with the Vonage Messages API using an Application ID and private key file. Create a Vonage Application in your dashboard, download the generated private.key file, and initialize the SDK with @vonage/auth using both credentials. Store the Application ID and private key path in environment variables via dotenv. The Messages API uses JWT authentication generated from these credentials, while the SDK also requires your API Key and Secret for initialization.
What Phone Number Format Does Vonage Messages API Require?
Vonage Messages API requires E.164 format for all phone numbers – the international standard starting with a plus sign followed by country code and national number, totaling 1-15 digits. Example: +14155550100 for a US number. Validate phone numbers using the regex /^\+[1-9]\d{1,14}$/ which enforces the plus sign, ensures the first digit isn't zero (per ITU-T specification), and limits total length to 15 digits. Reference: ITU-T E.164 Standard
How Do I Persist Scheduled SMS Reminders Across Server Restarts?
Persist scheduled reminders using SQLite or another database to store reminder details (recipient, message, scheduled time, status). On server startup, query the database for pending reminders with future send times and recreate the setTimeout calls to reschedule them. Store the reminder ID, recipient, message text, send time (ISO 8601 format), status, and creation/update timestamps. Update the status field ('pending', 'sent', 'failed', 'cancelled') as reminders progress through their lifecycle.
Can I Schedule SMS Messages More Than 24 Days in Advance?
Yes, but not directly with setTimeout due to its 24.8-day maximum delay constraint. Use a job queue library like Bull (Redis-backed) or Agenda (MongoDB-backed) for longer-term scheduling. These libraries periodically check for due jobs and execute them, supporting schedules months or years in advance. Alternatively, use a cron-based approach with node-cron to check your database daily for reminders due within the next 24 hours and schedule them with setTimeout.
How Do I Handle Failed SMS Sends in My Scheduling Service?
Handle failed SMS sends by wrapping the vonage.messages.send() call in a try-catch block within your setTimeout callback. Update the reminder status to 'failed' in your database or in-memory store when an error occurs. Log detailed error information from err.response.data for debugging. Optionally implement retry logic with exponential backoff for transient failures, or store failed reminders in a separate table/collection for manual review and reprocessing.
How Do I Deploy a Node.js SMS Scheduling Service to Production?
Deploy by choosing a hosting provider (Heroku, AWS EC2, Google Cloud Run, DigitalOcean), configuring environment variables securely on the server (never commit .env files), uploading your private key file with restricted permissions (chmod 600), implementing database persistence (SQLite for small scale, PostgreSQL/MySQL for larger deployments), using a process manager like PM2 to keep your app running, and setting up monitoring/logging. Ensure your server timezone is UTC for consistent timestamp handling across regions.
Frequently Asked Questions
How to install required dependencies for Vonage SMS scheduler?
Use npm install express @vonage/server-sdk dotenv node-cron uuid to install core dependencies. Optionally install sqlite3 for database, and express-validator for input validation.
What is the role of Express in a Node.js SMS scheduler?
Express.js acts as the web framework for handling requests and routing. It's used to set up the API endpoints for scheduling, canceling, and retrieving status updates of reminders.
How to schedule SMS reminders using Node.js?
You can schedule SMS reminders using Node.js with the help of libraries like Express.js for building a server and the Vonage Messages API for sending SMS messages. Use setTimeout or a cron job to trigger messages at the desired time.
What is the Vonage Messages API used for in Node.js?
The Vonage Messages API is used to programmatically send various types of messages, including SMS, from your Node.js applications. It requires an API key, secret, application ID, and private key for authentication.
Why does my Node.js SMS scheduler need a database?
A database provides persistent storage for scheduled SMS reminders. Without a database like SQLite, in-memory storage will lose all scheduled messages if the server restarts.
When should I use ngrok with my Vonage SMS service?
ngrok is helpful during local development to expose your server and receive webhooks from Vonage, such as message delivery receipts. While not strictly required for simply sending scheduled SMS, it's useful for handling replies and statuses.
Can I cancel a scheduled SMS reminder?
Yes, you can cancel pending reminders. An API endpoint can be created to look up the reminder ID and cancel it if the status is still pending.
How to set Vonage API credentials in my Node.js project?
Create a .env file and store Vonage credentials like API key, secret, application ID, private key path, and your virtual number. Load these variables using dotenv.
What is node-cron used for in SMS scheduling?
node-cron is used for tasks needing to be executed at regular intervals, like scheduled cleanups in your reminder application, although setTimeout is often sufficient for direct scheduling of individual messages.
What is the recommended way to handle authentication with Vonage Messages API?
Using Application ID and Private Key via Auth object for JWT generation is recommended for Vonage Messages API calls, even though SDK may also request API Key/Secret for initialization.
Why should I store Vonage private keys securely?
Vonage Private Keys are sensitive. Avoid storing them directly in your project; use environment variables with the key content or a secure secrets management service for production environments.
How to handle errors when sending SMS with Vonage API?
The sendSms function should include robust error handling. Log the error, including err.response.data if available, and rethrow or handle specific errors appropriately.
What is the purpose of the uuid library?
The uuid library generates unique identifiers for each reminder, ensuring that each scheduled message can be tracked and managed individually.
When should I add database persistence to the reminder service?
Database persistence is essential for production to avoid data loss on server restarts. While in-memory storage is convenient for testing, a database like SQLite provides reliable storage.
How does the input validation middleware work?
Input validation middleware checks for required fields (recipient, message, sendAt) and validates their format before scheduling reminders. Using express-validator is recommended for production for more robust validation.