code examples
code examples
Developer Guide: Building a Production-Ready SMS Scheduler with Node.js, Express, and Vonage
A step-by-step guide to creating an SMS scheduling application using Node.js, Express, Vonage API, node-cron, and PostgreSQL, covering setup, scheduling logic, database integration, and API creation.
This guide provides a step-by-step walkthrough for creating a robust SMS scheduling and reminder application using Node.js, Express, and the Vonage Messages API. We'll cover everything from project setup and core scheduling logic to database integration, error handling, security, deployment, and verification.
The goal is to build a system where users can schedule SMS messages to be sent at a specific future date and time via a simple API endpoint. This solves common needs like appointment reminders, notification scheduling, and future alerts.
Technologies Used:
- Node.js: JavaScript runtime environment.
- Express: Fast, unopinionated, minimalist web framework for Node.js.
- Vonage Messages API: For sending SMS messages reliably.
@vonage/server-sdk: The official Vonage Node.js SDK.node-cron: A simple cron-like job scheduler for Node.js.- PostgreSQL (or similar relational DB): For persistent storage of scheduled jobs. We'll use basic SQL examples, but an ORM like Prisma or Sequelize is recommended for larger projects.
dotenv: For managing environment variables.uuid: For generating unique job identifiers.
System Architecture:
+-----------+ +---------------------+ +-----------------+ +-------------+ +--------------+
| Client |----->| API Server (Express)|----->| Scheduler Logic |----->| Vonage API |----->| SMS Recipient|
| (e.g. App)| | |<---->| (node-cron) | +-------------+ +--------------+
+-----------+ | - API Endpoint |<---->| |
| - Validation | +-----------------+
| - DB Interaction |
+---------+-----------+
|
|
v
+---------------------+
| Database (Postgres) |
| - Scheduled Jobs |
+---------------------+(Note: ASCII diagrams may render differently depending on your viewer.)
Prerequisites:
- Node.js and npm/yarn: Installed on your system. Download Node.js
- Vonage API Account: Sign up for free if you don't have one. Vonage Signup
- 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 capable of sending SMS. Buy Numbers
- Database: A running instance of PostgreSQL (or adapt commands for MySQL, SQLite, etc.).
- PostgreSQL
pgcryptoExtension: Required forgen_random_uuid(). Ensure it's enabled in your database (CREATE EXTENSION IF NOT EXISTS 'pgcrypto';). - Text Editor/IDE: Like VS Code.
- (Optional)
ngrok: Useful for testing inbound webhooks if you extend this project later. ngrok - (Optional) Vonage CLI: Can be helpful for managing applications and numbers.
npm install -g @vonage/cli
1. Setting Up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory:
bashmkdir sms-scheduler cd sms-scheduler -
Initialize npm Project:
bashnpm init -y -
Install Dependencies:
bashnpm install express @vonage/server-sdk dotenv node-cron uuid pg # Or using yarn: # yarn add express @vonage/server-sdk dotenv node-cron uuid pgexpress: Web framework.@vonage/server-sdk: Vonage API client.dotenv: Loads environment variables from a.envfile.node-cron: Task scheduler.uuid: Generates unique IDs for jobs.pg: PostgreSQL client for Node.js (replace withmysql2,sqlite3, etc., if using a different DB).
-
Install Development Dependencies (Optional but Recommended):
bashnpm install --save-dev nodemon # Or using yarn: # yarn add --dev nodemonnodemon: Automatically restarts the server during development. Add a script topackage.json:json// package.json "scripts": { "start": "node server.js", "dev": "nodemon server.js" },
-
Create
.envFile: Create a file named.envin the project root to store sensitive credentials and configuration. Never commit this file to version control. Add a.gitignorefile with.envandnode_modules/.dotenv# .env # Server Configuration PORT=3000 # Vonage API Credentials VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_APPLICATION_ID=YOUR_VONAGE_APP_ID VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root VONAGE_SMS_FROM_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # e.g., +15551234567 # Database Connection (PostgreSQL Example) DATABASE_URL=postgresql://user:password@host:port/database_name # Security (Example API Key - use a more robust method for many keys) VALID_API_KEYS=your_secret_api_key_1,another_key_abc -
Set up Vonage Application:
- Go to your Vonage API Dashboard.
- Click "Create a new application".
- Give it a name (e.g., "SMSSchedulerApp").
- Click "Generate public and private key". Save the
private.keyfile that downloads into your project's root directory (or updateVONAGE_PRIVATE_KEY_PATHin.env). The public key is stored by Vonage. - Enable the "Messages" capability. You don't need webhook URLs for sending scheduled SMS, but if you plan to receive status updates or replies later, you'd enter your ngrok URLs here (e.g.,
https://<your-ngrok-id>.ngrok.io/webhooks/statusand/inbound). - Note the Application ID generated – add it to your
.envfile. - Scroll down to "Link virtual numbers" and link the Vonage number you purchased to this application. Add this number (in E.164 format, e.g.,
+15551234567) to your.envfile asVONAGE_SMS_FROM_NUMBER. - Important: Go to your main API Settings page. Under the "API keys" section, find SMS settings (the exact UI text like "SMS settings" or "Messages API" selection might change, look for where you configure default SMS behavior). Ensure the setting for sending SMS defaults to using the Messages API. Save changes. This ensures the SDK uses the correct backend API.
-
Project Structure (Basic):
textsms-scheduler/ ├── node_modules/ ├── config/ │ ├── vonage.js │ └── logger.js (Optional) ├── db/ │ └── jobs.js ├── routes/ │ └── scheduleRoutes.js ├── services/ │ └── schedulerService.js ├── .env ├── .gitignore ├── package.json ├── private.key # Your downloaded private key └── server.js # Main application file(We've added folders for better organization)
-
Basic Server Setup (
server.js):javascript// server.js require('dotenv').config(); // Load environment variables first const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(express.json()); // Parse JSON request bodies // Simple root route app.get('/', (req, res) => { res.send('SMS Scheduler API is running!'); }); // Start the server app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });Run
npm run dev(if using nodemon) ornpm startto test. You should see the console message and be able to accesshttp://localhost:3000in your browser.
2. Database Schema and Data Layer
We need a database table to store information about the scheduled SMS jobs.
-
Database Schema (
ScheduledJobsTable): We'll use PostgreSQL syntax. Adapt if needed for other databases.sql-- Connect to your database using psql or a GUI tool first -- Note: The use of gen_random_uuid() requires the pgcrypto extension. -- Enable it in your database if it's not already enabled: -- CREATE EXTENSION IF NOT EXISTS 'pgcrypto'; CREATE TABLE ScheduledJobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Or SERIAL for integer IDs recipient VARCHAR(20) NOT NULL, -- E.164 phone number format message TEXT NOT NULL, scheduled_time TIMESTAMPTZ NOT NULL, -- Timestamp with time zone (IMPORTANT!) status VARCHAR(15) NOT NULL DEFAULT 'pending', -- pending, processing, sent, failed, cancelled vonage_message_id VARCHAR(50) NULL, -- Store Vonage's ID after sending error_message TEXT NULL, -- Store error details on failure created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Create indexes for performance CREATE INDEX idx_scheduledjobs_status_time ON ScheduledJobs (status, scheduled_time);id: Unique identifier for the job (UUID recommended).recipient: Target phone number.message: SMS content.scheduled_time: The crucial field – when the SMS should be sent. UsingTIMESTAMPTZstores the timestamp in UTC along with time zone information, avoiding ambiguity.status: Tracks the job's lifecycle.vonage_message_id: Useful for tracking delivery status later.error_message: For debugging failures.- Indexes: Help quickly find pending jobs that need scheduling or checking.
-
Database Connection Setup: Create a simple database connection module.
javascript// db.js (create this file in the root or a 'config' folder) const { Pool } = require('pg'); const pool = new Pool({ connectionString: process.env.DATABASE_URL, // Optional: Add SSL configuration for production databases // ssl: { // rejectUnauthorized: false // Adjust as per your provider's requirements // } }); pool.on('connect', () => { console.log('Connected to the Database'); }); pool.on('error', (err) => { console.error('Database connection error:', err.stack); // Consider adding logic to attempt reconnection or exit gracefully // For critical apps, you might want to exit if the pool errors out badly // process.exit(1); }); module.exports = { query: (text, params) => pool.query(text, params), pool // Export pool if needed for transactions etc. }; -
Data Access Functions: Place these functions in
db/jobs.js.javascript// db/jobs.js const db = require('../db'); // Adjust path to db.js if needed const { v4: uuidv4 } = require('uuid'); async function createJob(recipient, message, scheduledTime) { const queryText = ` INSERT INTO ScheduledJobs (id, recipient, message, scheduled_time, status) VALUES ($1, $2, $3, $4, $5) RETURNING *; `; // Ensure scheduledTime is a valid Date object or ISO string parsable by Postgres const isoScheduledTime = new Date(scheduledTime).toISOString(); const values = [uuidv4(), recipient, message, isoScheduledTime, 'pending']; try { const res = await db.query(queryText, values); console.log(`Job created with ID: ${res.rows[0].id}`); return res.rows[0]; } catch (err) { console.error('Error creating job:', err); throw err; // Re-throw to be handled by caller } } async function findJobById(id) { const queryText = `SELECT * FROM ScheduledJobs WHERE id = $1;`; try { const res = await db.query(queryText, [id]); return res.rows[0]; // Returns the job object or undefined if not found } catch (err) { console.error(`Error finding job by ID ${id}:`, err); throw err; } } async function findPendingJobs() { // Fetch ALL pending jobs. Logic to handle past-due ones is in schedulerService. const queryText = ` SELECT * FROM ScheduledJobs WHERE status = 'pending' ORDER BY scheduled_time ASC; `; try { const res = await db.query(queryText); return res.rows; } catch (err) { console.error('Error fetching pending jobs:', err); throw err; } } async function updateJobStatus(id, status, vonageMessageId = null, errorMessage = null) { const queryText = ` UPDATE ScheduledJobs SET status = $1, vonage_message_id = $2, error_message = $3, updated_at = NOW() WHERE id = $4 RETURNING *; `; const values = [status, vonageMessageId, errorMessage, id]; try { const res = await db.query(queryText, values); if (res.rowCount === 0) { console.warn(`Job ID ${id} not found for status update.`); return null; } console.log(`Job ${id} status updated to ${status}`); return res.rows[0]; } catch (err) { console.error(`Error updating job ${id} status:`, err); throw err; } } module.exports = { createJob, findJobById, findPendingJobs, updateJobStatus, };(Note: This uses raw SQL. An ORM like Prisma or Sequelize simplifies this significantly).
3. Implementing Core Functionality: Scheduling and Sending
Now, let's integrate node-cron with our database jobs and Vonage.
-
Initialize Vonage SDK: Create
config/vonage.js.javascript// config/vonage.js const { Vonage } = require('@vonage/server-sdk'); const fs = require('fs'); const path = require('path'); // Use path for robust file path handling const privateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH ? path.resolve(process.env.VONAGE_PRIVATE_KEY_PATH) // Resolve relative to project root : null; let privateKey; if (privateKeyPath) { try { privateKey = fs.readFileSync(privateKeyPath); } catch (err) { console.error(`Error reading private key from ${privateKeyPath}:`, err); // Handle error appropriately - exit if key is essential process.exit(1); } } else { console.error('VONAGE_PRIVATE_KEY_PATH is not defined in .env'); process.exit(1); } if (!process.env.VONAGE_APPLICATION_ID || !privateKey) { console.error('Vonage Application ID or Private Key not configured or loaded correctly.'); process.exit(1); } const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKey // Pass the buffer directly }, { debug: process.env.NODE_ENV !== 'production' }); // Enable debug logging in non-prod console.log('Vonage SDK Initialized'); module.exports = vonage; -
Scheduling Service Logic: Create
services/schedulerService.js.javascript// services/schedulerService.js const cron = require('node-cron'); const vonage = require('../config/vonage'); // Use the initialized SDK const { createJob, findJobById, findPendingJobs, updateJobStatus } = require('../db/jobs'); // Use DB functions const scheduledTasks = new Map(); // Keep track of active cron jobs in memory // Function to send SMS via Vonage async function sendSms(jobId, recipient, message) { console.log(`Attempting to send SMS for job ${jobId} to ${recipient}`); await updateJobStatus(jobId, 'processing'); // Mark as processing try { const resp = await vonage.messages.send({ to: recipient, from: process.env.VONAGE_SMS_FROM_NUMBER, channel: 'sms', message_type: 'text', text: message, }); console.log(`SMS sent successfully for job ${jobId}. Vonage Message ID: ${resp.message_uuid}`); await updateJobStatus(jobId, 'sent', resp.message_uuid); return true; } catch (err) { // Extract meaningful error message from Vonage response if available const detail = err.response?.data?.detail || err.response?.data?.title || 'No specific detail provided'; const errorMessage = `Vonage API Error: ${err.message || 'Unknown error'} - ${detail}`; console.error(`Error sending SMS for job ${jobId}:`, errorMessage, err.response?.data); await updateJobStatus(jobId, 'failed', null, errorMessage); return false; } } // Function to schedule a single job using node-cron function scheduleCronJob(job) { if (scheduledTasks.has(job.id)) { console.log(`Job ${job.id} is already scheduled in memory. Skipping.`); return; // Avoid duplicate scheduling } const scheduledDate = new Date(job.scheduled_time); // Check if the scheduled time is in the past *upon scheduling* // This handles jobs loaded on startup that were missed during downtime. // Note: There's a minor potential race condition if the time is *extremely* close. if (scheduledDate <= new Date()) { console.warn(`Job ${job.id} scheduled time (${scheduledDate.toISOString()}) is in the past or now. Marking as failed.`); // Mark as failed instead of just skipping silently updateJobStatus(job.id, 'failed', null, 'Scheduled time already passed upon scheduler initialization').catch(err => { console.error(`Error updating status for past-due job ${job.id}:`, err); // Log error if status update fails }); return; } // Convert scheduledDate to cron pattern components const minute = scheduledDate.getUTCMinutes(); const hour = scheduledDate.getUTCHours(); const dayOfMonth = scheduledDate.getUTCDate(); const month = scheduledDate.getUTCMonth() + 1; // Cron months are 1-12 const dayOfWeek = '*'; // Run regardless of day of week const cronExpression = `${minute} ${hour} ${dayOfMonth} ${month} ${dayOfWeek}`; console.log(`Scheduling job ${job.id} with cron expression: ${cronExpression} for time: ${scheduledDate.toISOString()} (UTC)`); const task = cron.schedule(cronExpression, async () => { console.log(`Executing cron task for job ${job.id} at ${new Date().toISOString()}`); try { // Fetch latest job details to ensure it wasn't cancelled since scheduling const currentJob = await findJobById(job.id); if (currentJob && currentJob.status === 'pending') { await sendSms(job.id, job.recipient, job.message); } else { console.log(`Job ${job.id} status is '${currentJob?.status || 'not found'}', not 'pending'. Skipping send.`); // If cancelled or already processed, don't send. Status is already updated. } } catch (error) { console.error(`Unexpected error executing cron task for job ${job.id}:`, error); // Attempt to mark as failed if it wasn't already (e.g., DB error during findJobById) try { await updateJobStatus(job.id, 'failed', null, `Cron execution error: ${error.message}`); } catch (updateErr) { console.error(`Failed to update job ${job.id} status after execution error:`, updateErr); } } finally { // Remove the task from memory after execution attempt scheduledTasks.delete(job.id); // Explicitly stop the task to ensure it doesn't run again // (though with a specific date cron, it shouldn't) task.stop(); console.log(`Cron task stopped and removed for job ${job.id}`); } }, { scheduled: true, timezone: 'Etc/UTC' // IMPORTANT: Run cron based on UTC to match TIMESTAMPTZ }); scheduledTasks.set(job.id, task); // Store the task reference } // Function to load and schedule pending jobs on application startup async function initializeScheduler() { console.log('Initializing scheduler: Loading pending jobs...'); try { const pendingJobs = await findPendingJobs(); // Fetches all with status 'pending' console.log(`Found ${pendingJobs.length} pending jobs to potentially schedule.`); pendingJobs.forEach(job => { scheduleCronJob(job); // scheduleCronJob handles past-due jobs }); console.log('Scheduler initialization complete.'); } catch (error) { console.error('FATAL: Error initializing scheduler:', error); // Decide if this is a fatal error - often it is if jobs can't be loaded. throw error; // Propagate error to potentially stop server startup } } // Function called by the API to create and schedule a new job async function scheduleNewSms(recipient, message, scheduledTime) { // 1. Validation happens in the API route layer before calling this // 2. Create job in DB const newJob = await createJob(recipient, message, scheduledTime); // 3. Schedule the cron task scheduleCronJob(newJob); return newJob; // Return the created job details } // Function to cancel a job async function cancelScheduledSms(jobId) { const task = scheduledTasks.get(jobId); if (task) { task.stop(); scheduledTasks.delete(jobId); console.log(`Active cron task stopped and removed for job ${jobId}`); } else { console.warn(`No active cron task found in memory for job ${jobId} to cancel (might be already executed, failed, or server restarted).`); } // Update status in DB regardless (idempotent) console.log(`Attempting to mark job ${jobId} as cancelled in DB.`); return await updateJobStatus(jobId, 'cancelled'); } module.exports = { scheduleNewSms, initializeScheduler, cancelScheduledSms }; -
Initialize Scheduler on Startup: Modify
server.jsto callinitializeSchedulerand handle potential startup errors.javascript// server.js require('dotenv').config(); const express = require('express'); const db = require('./db'); // Import db module const { initializeScheduler } = require('./services/schedulerService'); // Import the initializer const scheduleRoutes = require('./routes/scheduleRoutes'); // Import routes const { apiKeyAuthMiddleware } = require('./middleware/auth'); // Import auth middleware (create next) const app = express(); const PORT = process.env.PORT || 3000; // --- Middleware --- app.use(express.json()); // Add other middleware like helmet, cors if needed // app.use(helmet()); // Example // --- Routes --- app.get('/', (req, res) => { res.send('SMS Scheduler API is running!'); }); app.get('/health', async (req, res) => { try { await db.query('SELECT 1'); // Check DB connection res.status(200).json({ status: 'OK', database: 'connected' }); } catch (error) { console.error('Health check failed:', error); res.status(503).json({ status: 'Error', database: 'disconnected', message: error.message }); } }); // Apply API Key Auth to schedule routes app.use('/api/schedule', apiKeyAuthMiddleware, scheduleRoutes); // --- Centralized Error Handler --- // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { console.error("Unhandled Error:", err.stack || err); const statusCode = err.statusCode || 500; const message = err.message || 'An unexpected internal server error occurred.'; res.status(statusCode).json({ status: 'error', statusCode: statusCode, message: message, }); }); // --- Server Startup and Scheduler Initialization --- const server = app.listen(PORT, async () => { console.log(`Server listening on port ${PORT}`); try { // Verify DB connection before proceeding await db.query('SELECT NOW()'); console.log('Database connection verified.'); // Initialize the scheduler after DB is confirmed await initializeScheduler(); } catch (err) { console.error("FATAL: Failed to connect to DB or initialize scheduler on startup:", err); // If DB connection or scheduler init is critical, exit the process // This prevents the server from running in a potentially broken state. server.close(() => { console.log('Server shutting down due to critical startup error.'); process.exit(1); }); } }); // --- Graceful Shutdown --- const shutdown = (signal) => { console.log(`${signal} signal received: closing HTTP server`); server.close(async () => { console.log('HTTP server closed'); try { await db.pool.end(); // Close DB connections console.log('Database pool closed'); } catch (err) { console.error('Error closing database pool:', err); } finally { process.exit(0); } }); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); // Handle Ctrl+C locally
4. Building the API Layer
Let's create the Express routes to interact with our scheduler.
-
Create API Routes: Place in
routes/scheduleRoutes.js.javascript// routes/scheduleRoutes.js const express = require('express'); const { scheduleNewSms, cancelScheduledSms } = require('../services/schedulerService'); const { findJobById } = require('../db/jobs'); // Use the implemented function const router = express.Router(); // Schedule a new SMS router.post('/', async (req, res, next) => { const { recipient, message, scheduledTime } = req.body; // --- Input Validation --- // Use a library like express-validator or joi for robust validation in production. if (!recipient || !message || !scheduledTime) { return res.status(400).json({ error: 'Missing required fields: recipient, message, scheduledTime (ISO 8601 format)' }); } // Basic ISO 8601 check and future date check if (isNaN(new Date(scheduledTime).getTime())) { return res.status(400).json({ error: 'Invalid scheduledTime format. Use ISO 8601 format (e.g., YYYY-MM-DDTHH:mm:ssZ).' }); } if (new Date(scheduledTime) <= new Date()) { return res.status(400).json({ error: 'Scheduled time must be in the future.' }); } // Basic E.164 format check (adjust regex as needed for strictness) if (!/^\+[1-9]\d{1,14}$/.test(recipient)) { return res.status(400).json({ error: 'Invalid recipient phone number format. Use E.164 format (e.g., +15551234567).' }); } // Consider adding length checks for the message if necessary. // --- End Validation --- try { const newJob = await scheduleNewSms(recipient, message, scheduledTime); // Respond with 202 Accepted: The request is accepted, processing will occur later. res.status(202).json({ message: 'SMS scheduled successfully.', jobId: newJob.id, status: newJob.status, scheduledTime: newJob.scheduled_time }); } catch (error) { console.error('API Error scheduling SMS:', error); // Pass error to central error handler next(error); } }); // Get Job Status by ID router.get('/:id', async (req, res, next) => { const { id } = req.params; // Basic ID validation (check if it looks like a UUID) if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id)) { return res.status(400).json({ error: 'Invalid job ID format.' }); } try { const job = await findJobById(id); // Use the implemented function if (!job) { return res.status(404).json({ error: 'Job not found' }); } res.status(200).json(job); // Return the job details } catch (error) { console.error(`API Error getting job ${id}:`, error); next(error); // Pass to error handler } }); // Cancel a Scheduled SMS router.delete('/:id', async (req, res, next) => { const { id } = req.params; // Basic ID validation if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id)) { return res.status(400).json({ error: 'Invalid job ID format.' }); } try { const cancelledJob = await cancelScheduledSms(id); if (!cancelledJob) { // Could be not found, or already processed/cancelled (updateJobStatus returns null if not found) // Check the job status directly if more specific feedback is needed const currentJob = await findJobById(id); if (!currentJob) { return res.status(404).json({ error: 'Job not found.' }); } else { // Job exists but wasn't pending or failed to update (unlikely if findJobById worked) return res.status(409).json({ // 409 Conflict might be suitable message: `Job found but could not be cancelled (current status: ${currentJob.status}).`, jobId: currentJob.id, status: currentJob.status }); } } // If cancelScheduledSms returned the job, it means the status was updated successfully res.status(200).json({ message: 'Scheduled SMS cancelled successfully.', jobId: cancelledJob.id, status: cancelledJob.status // Should be 'cancelled' }); } catch (error) { console.error(`API Error cancelling job ${id}:`, error); next(error); } }); module.exports = router; -
Mount Routes in
server.js: (Already done in the updatedserver.jsin Section 3). -
Testing with
curlor Postman: Start your server (npm run dev).-
Schedule SMS (requires valid API Key in header):
bash# Replace YOUR_API_KEY with one from your .env curl -X POST http://localhost:3000/api/schedule \ -H ""Content-Type: application/json"" \ -H ""X-API-Key: YOUR_API_KEY"" \ -d '{ ""recipient"": ""+15559876543"", ""message"": ""Your appointment is tomorrow at 10 AM."", ""scheduledTime"": ""2025-04-21T10:00:00Z"" # Use a future ISO 8601 date/time in UTC }'(Replace recipient number, API key, and adjust time) You should get a
202 Acceptedresponse with the job ID. Check server logs and database. -
Get Job Status (requires valid API Key):
bash# Replace YOUR_JOB_ID and YOUR_API_KEY curl -X GET http://localhost:3000/api/schedule/YOUR_JOB_ID_HERE \ -H ""X-API-Key: YOUR_API_KEY""You should get a
200 OKresponse with job details or404 Not Found.
-
Frequently Asked Questions
How to schedule SMS messages with Node.js?
Use Node.js with Express, the Vonage Messages API, and node-cron to schedule SMS messages. Set up an Express server to handle API requests, use the Vonage API to send messages, and node-cron to trigger messages at scheduled times, storing job details in a database like PostgreSQL.
What is the Vonage Messages API used for?
The Vonage Messages API is a service that enables sending SMS messages programmatically. It's integrated into the Node.js application using the official Vonage Node.js SDK, @vonage/server-sdk, simplifying the process of sending text messages.
Why use node-cron in an SMS scheduler?
Node-cron provides a simple way to schedule tasks in a Node.js application, similar to cron jobs on a server. It's used to trigger the sending of scheduled SMS messages at specific times defined by cron expressions.
How to set up a Vonage application for SMS scheduling?
Create a Vonage application, enable the Messages capability, download your private key, and link your virtual number. Add the Application ID, private key path, and virtual number to your .env file, which Vonage uses to send scheduled messages. Go to Messages API settings and configure SMS defaults to use the Messages API
What database is recommended for storing scheduled SMS jobs?
PostgreSQL, or similar relational databases, are recommended for persistent storage. The guide provides SQL examples for PostgreSQL but suggests using an ORM like Prisma or Sequelize for more complex projects.
What is the purpose of the pgcrypto extension in PostgreSQL?
The pgcrypto extension is required to use the gen_random_uuid() function, which generates UUIDs for identifying scheduled SMS jobs. Enable it with `CREATE EXTENSION IF NOT EXISTS 'pgcrypto';`
How to install required dependencies for this project?
Run `npm install express @vonage/server-sdk dotenv node-cron uuid pg` or `yarn add express @vonage/server-sdk dotenv node-cron uuid pg` to install necessary dependencies for the SMS scheduling project, such as Express, Vonage SDK, and database connector.
What is the role of dotenv in the project setup?
The dotenv package helps manage environment variables. It loads variables from a .env file into process.env, allowing you to keep sensitive credentials like API keys and database connection strings separate from code. Ensure this file is in your .gitignore
How to handle past-due scheduled SMS messages?
The scheduler service checks for scheduled times that have already passed when the application starts or a job is scheduled. If a past-due job is found, it's marked as 'failed' in the database, preventing attempts to send the outdated message.
What are the required fields when scheduling a new SMS message?
The '/schedule' API endpoint expects 'recipient', 'message', and 'scheduledTime' in the request body. The recipient must be in E.164 format, and scheduledTime should be a future date and time in ISO 8601 format.
When should I use ngrok with the Vonage API?
ngrok is helpful during development to expose your local server and receive inbound webhooks from Vonage, like status updates or message replies, which are not necessary for the basic sending of scheduled SMS messages, but are helpful for future enhancements.
How to cancel a scheduled SMS message?
Send a DELETE request to `/api/schedule/YOUR_JOB_ID` (ensure a valid API Key). This will update the job status to 'cancelled', preventing the scheduled task from sending the message, and it will stop the task in memory on the current server.
Can I change the database from PostgreSQL to something else?
The examples use PostgreSQL syntax, but you can adapt the SQL commands and the `db.js` configuration to use MySQL, SQLite, or other databases if preferred. You'll need the appropriate database client for Node (`mysql2`, `sqlite3`, etc.).
What does '202 Accepted' mean in the API response?
A 202 Accepted response to an API request to schedule an SMS indicates that the request has been accepted for processing but hasn't been completed yet. The SMS will be sent at the specified scheduled time.