This guide provides a step-by-step walkthrough for creating a Node.js application using the Express framework to schedule SMS messages for future delivery via the Vonage Messages API. We'll build an API endpoint that accepts scheduling requests, manages pending messages, and uses Vonage to send them at the specified time.
Project Overview and Goals
What We're Building:
We are building a backend API service that enables users to schedule SMS messages. The core functionality includes:
- An API endpoint (
POST /schedule
) to accept SMS scheduling requests (recipient number, message content, desired send time). - A scheduling mechanism (
node-cron
) to trigger SMS sending at the correct time. - Integration with the Vonage Messages API to send the SMS messages.
- Basic in-memory storage for scheduled jobs (with caveats discussed for production).
- Error handling, logging, and security considerations.
Problem Solved:
This application addresses the need to send automated SMS communications at specific future dates and times, such as appointment reminders, follow-up messages, or time-sensitive alerts, without requiring manual intervention at the moment of sending.
Technologies Used:
- Node.js: A JavaScript runtime for building server-side applications.
- Express: A minimal and flexible Node.js web application framework for building the API.
- Vonage Messages API: A powerful communication API for sending messages across various channels, including SMS. We'll use the
@vonage/server-sdk
. node-cron
: A simple cron-like job scheduler for Node.js to trigger tasks at specific times.dotenv
: To manage environment variables securely.uuid
: To generate unique identifiers for scheduled jobs.
Why Vonage?
Vonage provides reliable and scalable communication APIs with extensive features and global reach, making it a solid choice for integrating SMS capabilities into applications. The Messages API offers a unified way to handle multiple channels, although we focus on SMS here.
System Architecture:
+-----------+ +-------------------+ +-----------------+ +-----------------+ +--------------+
| Client |------>| Node.js/Express |----->| Scheduling Logic|----->| Vonage Service |----->| Vonage Cloud |
| (e.g. curl| | API Server | | (node-cron) | | (SDK Wrapper) | | (Messages API|
| Postman) |<------| (Listens on Port) |<-----| (In-Memory Store| +-----------------+ +------|-------+
+-----------+ +-------------------+ +-----------------+ |
| SMS
v
+---------------+
| SMS Recipient |
+---------------+
Prerequisites:
- Node.js and npm (or yarn): Installed on your system. (Download Node.js)
- Vonage API Account: Required to get API credentials and a virtual number. (Sign up for Vonage - free credit available for new accounts).
- Vonage CLI: Optional but recommended for managing applications and numbers. Install via npm:
npm install -g @vonage/cli
- ngrok (Optional): Useful for testing webhooks locally if you extend the app to handle status updates or replies. (ngrok Website)
- Basic knowledge of JavaScript_ Node.js_ and REST APIs.
Final Outcome:
By the end of this guide_ you will have a running Node.js Express application with a /schedule
endpoint capable of accepting SMS scheduling requests and sending the messages at the specified future time using Vonage.
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 your project_ then navigate into it.
mkdir vonage-sms-scheduler
cd vonage-sms-scheduler
Step 2: Initialize Node.js Project
Initialize the project using npm (or yarn). The -y
flag accepts default settings.
npm init -y
This creates a package.json
file.
Step 3: Install Dependencies
Install Express_ the Vonage Server SDK_ node-cron
_ dotenv
_ and uuid
.
npm install express @vonage/server-sdk node-cron dotenv uuid
express
: Web framework.@vonage/server-sdk
: Official Vonage SDK for Node.js.node-cron
: Task scheduler.dotenv
: Loads environment variables from a.env
file.uuid
: Generates unique IDs for tracking jobs.
Step 4: Project Structure (Recommended)
Create a basic structure for better organization:
mkdir src
mkdir src/routes
mkdir src/services
mkdir src/config
src/
: Contains your main application code.src/routes/
: Holds Express route definitions.src/services/
: Contains business logic_ like interacting with Vonage or the scheduler.src/config/
: For configuration files (like Vonage SDK initialization).
Step 5: Create Basic Express Server (src/app.js
)
Create a file named app.js
inside the src
directory with the following basic Express setup:
// src/app.js
require('dotenv').config(); // Load environment variables early
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000; // Use port from env or default to 3000
// Middleware
app.use(express.json()); // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// Simple Root Route for testing
app.get('/'_ (req_ res) => {
res.status(200).json({ message: 'Vonage SMS Scheduler API is running!' });
});
// --- Routes will be added here later ---
// Start the server
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
// Export app for potential testing
module.exports = app;
Step 6: Create Start Script
Modify the scripts
section in your package.json
to easily start the server:
// package.json
{
// ... other properties
"main": "src/app.js", // Point to your entry file
"scripts": {
"start": "node src/app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
// ... other properties
}
You can now test the basic server by running npm start
in your terminal and navigating to http://localhost:3000
in your browser or using curl http://localhost:3000
. You should see the JSON message. Stop the server with Ctrl+C
.
2. Setting Up Vonage Credentials and SDK
To interact with the Vonage API, you need credentials and a virtual phone number. We'll use the Application ID and Private Key method, which is recommended for the Messages API.
Step 1: Create a Vonage Application
An Application acts as a container for your communication settings and credentials.
- Using the Vonage CLI (Recommended):
- Configure CLI: If you haven't already, link the CLI to your account:
(Find your Key and Secret in the Vonage Dashboard)
vonage config:set --apiKey=YOUR_API_KEY --apiSecret=YOUR_API_SECRET
- Create Application: Run the following command. It will prompt you for a name and generate keys.
vonage apps:create ""SMS Scheduler App"" --keyfile=private.key
- It will ask about capabilities; you don't strictly need Messages webhooks for sending scheduled messages, but enabling them (
Messages
capability) is useful if you plan to add status updates later. If prompted for webhook URLs, you can provide placeholders for now (e.g.,https://example.com/webhooks/inbound
,https://example.com/webhooks/status
). - Important: This command creates
private.key
in your current directory and outputs an Application ID. Save this ID.
- It will ask about capabilities; you don't strictly need Messages webhooks for sending scheduled messages, but enabling them (
- Configure CLI: If you haven't already, link the CLI to your account:
- Using the Vonage Dashboard:
- Navigate to ""Your applications"" -> ""Create a new application"".
- Enter a name (e.g., ""SMS Scheduler App"").
- Click ""Generate public and private key"". Save the downloaded
private.key
file securely (e.g., in your project root, but ensure it's gitignored). The public key remains with Vonage. - Note the Application ID displayed on the page.
- Optionally, enable the ""Messages"" capability and enter placeholder webhook URLs if desired.
- Click ""Create application"".
Step 2: Obtain a Vonage Virtual Number
You need a Vonage phone number to send SMS messages from.
- Using the Vonage CLI:
- Search for a number (replace
US
with your desired country code):vonage numbers:search US --features=SMS
- Buy a number from the list (replace
NUMBER
andCOUNTRY_CODE
):Note the number you purchased.vonage numbers:buy 15551234567 US
- Search for a number (replace
- Using the Vonage Dashboard:
- Navigate to ""Numbers"" -> ""Buy numbers"".
- Select country, features (SMS), and type (Mobile recommended).
- Click ""Search"" and buy a suitable number. Note it down.
Step 3: Link the Number to Your Application
Associate the purchased number with the application you created. This tells Vonage which application's settings (and potentially webhooks) to use for messages involving this number.
- Using the Vonage CLI:
(Replace with your actual number and Application ID).
vonage apps:link --number=YOUR_VONAGE_NUMBER YOUR_APPLICATION_ID
- Using the Vonage Dashboard:
- Go to ""Your applications"" and click on your ""SMS Scheduler App"".
- Scroll down to the ""Link virtual numbers"" section.
- Find your number and click the ""Link"" button.
Step 4: Configure Environment Variables
Create a file named .env
in the root of your project (the same level as package.json
). Never commit this file to Git.
# .env
# Vonage Credentials
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
# Vonage Number to send SMS from
VONAGE_SMS_FROM_NUMBER=YOUR_VONAGE_NUMBER
# Optional: Port for the server
# PORT=3000
- Replace
YOUR_API_KEY
,YOUR_API_SECRET
,YOUR_APPLICATION_ID
, andYOUR_VONAGE_NUMBER
with your actual values. - Ensure
VONAGE_PRIVATE_KEY_PATH
points correctly to where you savedprivate.key
. - Add
.env
andprivate.key
to your.gitignore
file:
# .gitignore
node_modules
.env
private.key
npm-debug.log
Step 5: Initialize Vonage SDK (src/config/vonageClient.js
)
Create a configuration file to initialize the SDK instance using your Application ID and Private Key.
// src/config/vonageClient.js
const { Vonage } = require('@vonage/server-sdk');
const { Auth } = require('@vonage/auth');
const fs = require('fs');
// Validate essential environment variables
if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) {
console.error('Error: VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH must be set in .env');
process.exit(1); // Exit if critical config is missing
}
let privateKey;
try {
privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
} catch (error) {
console.error(`Error reading private key from ${process.env.VONAGE_PRIVATE_KEY_PATH}:`, error.message);
process.exit(1);
}
const credentials = new Auth({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKey,
// Optional: Use API Key/Secret as fallback or alternative if needed
// apiKey: process.env.VONAGE_API_KEY,
// apiSecret: process.env.VONAGE_API_SECRET
});
const vonage = new Vonage(credentials);
console.log('Vonage SDK initialized successfully.');
module.exports = vonage;
This setup reads the private key file and uses the Application ID for authentication, which is the standard way for the Messages API. We also added basic checks to ensure critical environment variables are present.
3. Implementing the Scheduling Logic
We'll use node-cron
to run tasks at specific times and a simple in-memory object to keep track of scheduled jobs.
Important Caveat: This in-memory storage is not suitable for production. If the server restarts, all scheduled jobs will be lost. For production, use a persistent store like Redis or a database, potentially with a dedicated job queue library (e.g., BullMQ, Agenda).
Step 1: Create Scheduler Service (src/services/schedulerService.js
)
This service will manage the scheduling and storage of jobs.
// src/services/schedulerService.js
const cron = require('node-cron');
const { v4: uuidv4 } = require('uuid');
const smsService = require('./smsService'); // We will create this next
// In-memory store for scheduled jobs { jobId: { task: cronTask, details: {...} } }
const scheduledJobs = {};
/**
* Schedules an SMS message to be sent at a specific time.
* @param {string} recipient - The phone number to send the SMS to.
* @param {string} message - The content of the SMS.
* @param {Date} sendAt - The Date object representing when to send the message.
* @returns {string} - The unique ID of the scheduled job.
* @throws {Error} - If sendAt is in the past or invalid.
*/
const scheduleSms = (recipient, message, sendAt) => {
const now = new Date();
if (!(sendAt instanceof Date) || isNaN(sendAt) || sendAt <= now) {
throw new Error('Invalid or past date provided for scheduling.');
}
const jobId = uuidv4();
// Use node-cron's ability to schedule based on a Date object
const task = cron.schedule(sendAt_ async () => {
console.log(`[Scheduler] Executing job ${jobId} at ${new Date()}`);
try {
await smsService.sendSms(recipient, message);
console.log(`[Scheduler] Job ${jobId}: SMS sent successfully to ${recipient}.`);
} catch (error) {
console.error(`[Scheduler] Job ${jobId}: Failed to send SMS to ${recipient}. Error: ${error.message}`);
// Optional: Implement retry logic here or log for manual intervention
} finally {
// Clean up the job from memory after execution (or attempted execution)
delete scheduledJobs[jobId];
console.log(`[Scheduler] Job ${jobId} removed from memory.`);
}
}, {
scheduled: true,
timezone: "Etc/UTC" // Explicitly use UTC or your desired timezone
});
scheduledJobs[jobId] = {
task: task,
details: {
jobId,
recipient,
message, // Storing message in memory; consider security for sensitive data
sendAt: sendAt.toISOString(),
createdAt: now.toISOString()
}
};
console.log(`[Scheduler] SMS for ${recipient} scheduled successfully. Job ID: ${jobId}, Send At: ${sendAt.toISOString()}`);
// Optional: Start the task explicitly if needed, though `scheduled: true` should handle it.
// task.start(); // Usually not needed with `scheduled: true` and a future date
return jobId;
};
/**
* Cancels a previously scheduled job.
* @param {string} jobId - The ID of the job to cancel.
* @returns {boolean} - True if cancelled, false if job not found.
*/
const cancelSms = (jobId) => {
const job = scheduledJobs[jobId];
if (job) {
job.task.stop(); // Stop the cron task
delete scheduledJobs[jobId];
console.log(`[Scheduler] Job ${jobId} cancelled successfully.`);
return true;
}
console.log(`[Scheduler] Job ${jobId} not found for cancellation.`);
return false;
};
/**
* Lists currently scheduled jobs (for debugging/monitoring).
* @returns {object[]} - An array of job details.
*/
const listScheduledJobs = () => {
return Object.values(scheduledJobs).map(job => job.details);
};
module.exports = {
scheduleSms,
cancelSms,
listScheduledJobs,
};
Explanation:
scheduledJobs
: An object acting as our simple in-memory database. Keys arejobId
, values contain thenode-cron
task object and job details.scheduleSms
:- Takes recipient, message, and a
Date
object (sendAt
). - Validates that
sendAt
is a valid future date. - Generates a unique
jobId
usinguuid
. - Uses
cron.schedule(sendAt, ...)
to schedule the task directly for the specified date and time. Crucially, we provide aDate
object here, whichnode-cron
supports for one-off tasks. - The function passed to
cron.schedule
is what runs atsendAt
. It calls oursmsService.sendSms
function (defined next). - Includes basic logging and removes the job from
scheduledJobs
after execution usingfinally
. - Sets
timezone
explicitly (UTC is recommended for servers). - Stores the task and details in
scheduledJobs
. - Returns the
jobId
.
- Takes recipient, message, and a
cancelSms
: Finds a job by ID, stops the underlyingcron
task usingtask.stop()
, and removes it from memory.listScheduledJobs
: Returns details of all pending jobs (useful for an admin/status endpoint).
4. Implementing the SMS Sending Service
This service encapsulates the interaction with the Vonage SDK to send the SMS.
Step 1: Create SMS Service (src/services/smsService.js
)
// src/services/smsService.js
const vonage = require('../config/vonageClient'); // Import the initialized SDK client
// Ensure the sender number is configured
const fromNumber = process.env.VONAGE_SMS_FROM_NUMBER;
if (!fromNumber) {
console.error('Error: VONAGE_SMS_FROM_NUMBER must be set in .env');
process.exit(1);
}
/**
* Sends an SMS message using the Vonage Messages API.
* @param {string} recipient - The phone number to send the SMS to (E.164 format recommended).
* @param {string} message - The content of the SMS.
* @returns {Promise<object>} - The response from the Vonage API.
* @throws {Error} - If sending fails.
*/
const sendSms = async (recipient, message) => {
console.log(`[SMS Service] Attempting to send SMS to ${recipient}`);
try {
const response = await vonage.messages.send({
channel: 'sms',
message_type: 'text',
to: recipient,
from: fromNumber,
text: message,
});
console.log(`[SMS Service] Message sent successfully. Message UUID: ${response.message_uuid}`);
return response; // Contains message_uuid
} catch (error) {
console.error(`[SMS Service] Error sending SMS to ${recipient}:`, error.response ? error.response.data : error.message);
// Rethrow the error so the scheduler knows it failed
throw new Error(`Failed to send SMS via Vonage: ${error.message}`);
}
};
module.exports = {
sendSms,
};
Explanation:
- Imports the configured
vonage
client instance. - Retrieves the
VONAGE_SMS_FROM_NUMBER
from environment variables and exits if not set. - The
sendSms
function isasync
as the SDK call is asynchronous (vonage.messages.send
returns a Promise). - It uses
vonage.messages.send
with the required parameters for SMS:channel
,message_type
,to
,from
,text
. - Includes
try...catch
for error handling. It logs detailed errors (including response data from Vonage if available) and rethrows a generic error to signal failure to the calling function (the scheduler). - Logs success, including the
message_uuid
returned by Vonage, which is useful for tracking.
5. Building the API Layer (Express Route)
Now, let's create the Express route that will use our scheduler service.
Step 1: Create Schedule Route (src/routes/scheduleRoutes.js
)
// src/routes/scheduleRoutes.js
const express = require('express');
const schedulerService = require('../services/schedulerService');
const router = express.Router();
// POST /api/schedule - Schedule a new SMS
router.post('/', (req, res, next) => {
const { recipient, message, sendAt } = req.body;
// --- Basic Input Validation ---
if (!recipient || !message || !sendAt) {
return res.status(400).json({ error: 'Missing required fields: recipient, message, sendAt (ISO 8601 format string).' });
}
let sendAtDate;
try {
sendAtDate = new Date(sendAt);
if (isNaN(sendAtDate)) {
throw new Error('Invalid date format.');
}
} catch (error) {
return res.status(400).json({ error: 'Invalid date format for sendAt. Please use ISO 8601 format (e.g., YYYY-MM-DDTHH:mm:ssZ).' });
}
const now = new Date();
if (sendAtDate <= now) {
return res.status(400).json({ error: 'Scheduled time must be in the future.' });
}
// --- End Basic Validation ---
try {
const jobId = schedulerService.scheduleSms(recipient_ message_ sendAtDate);
res.status(202).json({ // 202 Accepted: Request taken_ processing will happen later
message: 'SMS scheduled successfully.'_
jobId: jobId_
details: {
recipient: recipient_
// message: message_ // SECURITY NOTE: Avoid returning sensitive message content in production APIs.
sendAt: sendAtDate.toISOString()_
}
});
} catch (error) {
// Catch errors from schedulerService (e.g._ invalid date)
console.error('[API Route /schedule] Error scheduling SMS:'_ error);
// Pass to the global error handler (defined later) or send a specific response
// For now_ send a 500_ but could be 400 if it was a validation error caught in the service
res.status(500).json({ error: 'Failed to schedule SMS.'_ details: error.message });
// Alternatively: next(error); // If using global error middleware
}
});
// GET /api/schedule - List pending jobs (for admin/debug)
router.get('/'_ (req_ res) => {
const jobs = schedulerService.listScheduledJobs();
res.status(200).json({ scheduledJobs: jobs });
});
// DELETE /api/schedule/:jobId - Cancel a scheduled job
router.delete('/:jobId', (req, res) => {
const { jobId } = req.params;
if (!jobId) {
return res.status(400).json({ error: 'Job ID is required.' });
}
const cancelled = schedulerService.cancelSms(jobId);
if (cancelled) {
res.status(200).json({ message: `Job ${jobId} cancelled successfully.` });
} else {
res.status(404).json({ error: `Job ${jobId} not found or already executed/cancelled.` });
}
});
module.exports = router;
Explanation:
- Imports
express.Router
and ourschedulerService
. POST /
route:- Extracts
recipient
,message
, andsendAt
from the request body (req.body
). - Basic Validation: Checks if fields exist and if
sendAt
is a valid ISO 8601 date string that represents a future time. For production, use a dedicated validation library likeexpress-validator
orjoi
. - Calls
schedulerService.scheduleSms
with the validated data. - Responds with
202 Accepted
status code (appropriate for tasks accepted for later processing) and thejobId
. Note: The example response omits themessage
field for security reasons; returning potentially sensitive message content in API responses is generally discouraged in production. - Includes basic error handling for synchronous validation errors and catches errors from the service.
- Extracts
GET /
route: CallsschedulerService.listScheduledJobs
to return details of pending jobs.DELETE /:jobId
route: ExtractsjobId
from URL parameters (req.params
), callsschedulerService.cancelSms
, and returns success or 404 if the job wasn't found/cancelled.
Step 2: Mount the Router in app.js
Update src/app.js
to use this router.
// src/app.js
require('dotenv').config();
const express = require('express');
const scheduleRoutes = require('./routes/scheduleRoutes'); // Import the router
// Import other routers if you add more features
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => {
res.status(200).json({ message: 'Vonage SMS Scheduler API is running!' });
});
// --- Mount API Routes ---
app.use('/api/schedule', scheduleRoutes); // Use the schedule router for paths starting with /api/schedule
// --- Global Error Handler (Example) ---
// Add this AFTER your routes
app.use((err, req, res, next) => {
console.error('[Global Error Handler]:', err.stack || err);
// Avoid sending stack trace in production
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({ error: message });
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
// Initialize Vonage SDK (if not already done elsewhere)
require('./config/vonageClient');
// Initialize Scheduler (though node-cron tasks start automatically)
console.log('Scheduler service initialized.');
});
module.exports = app;
We added the scheduleRoutes
under the /api/schedule
path and included a basic global error handler middleware.
6. Error Handling, Logging, and Retries
While basic error handling is included, production apps need more robustness.
- Consistent Error Strategy: Use the global error handler middleware (added in
app.js
) to catch unhandled errors. Define custom error classes if needed (e.g.,ValidationError
,NotFoundError
). - Logging: Replace
console.log
/console.error
with a structured logger likepino
orwinston
. This enables different log levels (debug, info, warn, error), formatting (JSON), and sending logs to files or external services.- Example with
pino
:npm install pino pino-pretty
// src/config/logger.js (Example) const pino = require('pino'); const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: { target: 'pino-pretty', // Makes logs readable in development options: { colorize: true } } }); module.exports = logger; // Use it like: // const logger = require('./config/logger'); // logger.info('Server started'); // logger.error({ err: error }, 'Failed to send SMS');
- Example with
- Retries: The current
schedulerService
logs errors but doesn't retry. For critical reminders, implement a retry mechanism within thecatch
block of thecron.schedule
task function.- Simple Retry: A basic
for
loop with delays. - Exponential Backoff: Increase the delay between retries (e.g., 1s, 2s, 4s, 8s). Libraries like
async-retry
can simplify this. - Consider: How many retries? What maximum delay? What to do after final failure (e.g., log to an error queue, notify admin)?
- Simple Retry: A basic
7. Adding Security Features
- Input Validation: Use a dedicated library (
express-validator
,joi
) for robust validation ofrecipient
(E.164 format),message
(length, content), andsendAt
. Sanitize inputs to prevent injection attacks (though less critical for SMS content itself, it's good practice). - Rate Limiting: Protect the
/api/schedule
endpoint from abuse. Useexpress-rate-limit
.npm install express-rate-limit
// src/app.js (add before routes) const rateLimit = require('express-rate-limit'); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again after 15 minutes' }); // Apply to all API routes or specific ones app.use('/api/', apiLimiter);
- API Key/Authentication: Currently, the API is open. For production, protect it with API keys, JWT tokens, or other authentication mechanisms relevant to your application's architecture.
- Secure Headers: Use middleware like
helmet
(npm install helmet
) to set various HTTP headers for security (XSS protection, disabling content sniffing, etc.).// src/app.js const helmet = require('helmet'); app.use(helmet());
8. Database Schema and Persistence (Production Consideration)
As highlighted, the in-memory scheduledJobs
store is not persistent. For production:
- Choose a Database:
- Redis: Excellent for caching and simple key-value storage. Good for job queues if combined with libraries like BullMQ.
- PostgreSQL/MongoDB: More robust for storing job details, status, results, and potentially retry counts.
- Schema/Data Model: You'd need a table/collection for
ScheduledJobs
with fields like:jobId
(Primary Key, UUID)recipient
(String)message
(Text)sendAt
(Timestamp/DateTime)status
(Enum: 'PENDING', 'SENT', 'FAILED', 'CANCELLED')createdAt
(Timestamp)updatedAt
(Timestamp)vonageMessageUuid
(String, nullable - store after successful send)failureReason
(Text, nullable)retryCount
(Integer, default 0)
- Job Queue Library: Libraries like
BullMQ
(Redis-based) orAgenda
(MongoDB-based) handle job persistence, scheduling, retries, concurrency, and worker processes more reliably thannode-cron
with manual persistence logic. They often integrate better with database storage.
9. Verification and Testing
Step 1: Manual Verification (curl
/ Postman)
-
Start the server:
npm start
-
Schedule an SMS: Send a POST request to
http://localhost:3000/api/schedule
.- Using
curl
:(Replacecurl -X POST http://localhost:3000/api/schedule \ -H "Content-Type: application/json" \ -d '{ "recipient": "YOUR_PERSONAL_PHONE_NUMBER", "message": "Hello from the Vonage Scheduler! Testing 123.", "sendAt": "2025-04-20T18:30:00Z" }'
YOUR_PERSONAL_PHONE_NUMBER
and adjust thesendAt
time to be a minute or two in the future using ISO 8601 format - UTC 'Z' or specify offset). - Using Postman: Create a POST request, set the URL, select Body -> raw -> JSON, and paste the JSON payload.
- Using
-
Check Response: You should get a
202 Accepted
response with ajobId
. -
Check Logs: Monitor the console output where
npm start
is running. You should see logs for scheduling, and then later for execution and sending (or failure). -
Check Phone: Verify that you receive the SMS on
YOUR_PERSONAL_PHONE_NUMBER
at approximately the scheduled time.
Step 2: List Pending Jobs
Send a GET request to http://localhost:3000/api/schedule
. You should see the job you just scheduled (until it executes).
curl http://localhost:3000/api/schedule
Step 3: Cancel a Job
- Schedule another job far in the future. Note its
jobId
from the response. - Send a DELETE request to
http://localhost:3000/api/schedule/YOUR_JOB_ID
.
curl -X DELETE http://localhost:3000/api/schedule/paste_job_id_here
- Check the response (should be 200 OK).
- List jobs again (GET
/api/schedule
); the cancelled job should be gone.
Step 4: Unit & Integration Tests (Recommended)
For robust applications, write automated tests:
- Unit Tests (
jest
,mocha
): Test individual functions in isolation. Mock dependencies likenode-cron
and thevonage
SDK. Test validation logic, date parsing, error handling within services. - Integration Tests (
supertest
): Test the API endpoints. Start the Express server, send HTTP requests usingsupertest
, and assert responses. Mock external dependencies (vonage
SDK) to avoid actual API calls.