This guide provides a complete walkthrough for building a robust SMS scheduling and reminder application using Node.js, Express, and the Vonage Messages API. You'll learn how to create a service that accepts requests to send SMS messages at specific future times, handle potential issues, and deploy it reliably.
This application solves the common need to automate time-sensitive SMS communications, such as appointment reminders, follow-up messages, or scheduled notifications, without requiring manual intervention at the exact moment of sending. We'll use Node.js for its efficient asynchronous nature, Express for building a clean API layer, node-cron
for reliable scheduling, and the Vonage Messages API for sending the SMS messages. While the title suggests ""Production-Ready"", please note the core implementation uses an in-memory store for scheduled jobs, which is suitable for demonstration but requires replacement with a persistent database (discussed in Section 6) for true production reliability.
System Architecture:
graph LR
A[User/Client] -- HTTP POST /schedule --> B(Node.js/Express API);
B -- Stores Job Details & Schedules --> C{node-cron};
C -- Triggers at Scheduled Time --> D(Vonage SDK);
D -- Sends SMS --> E(Vonage Messages API);
E -- Delivers --> F[Recipient's Phone];
B -- (Optional) Logs Status --> G[Logging Service];
B -- (Optional) Stores Job State --> H[Database];
subgraph Your Application
B
C
D
G
H
end
subgraph External Services
E
end
Prerequisites:
- A Vonage API account (Sign up here).
- Your Vonage API Key and Secret (found on your Vonage Dashboard).
- Node.js and npm (or yarn) installed locally.
- A Vonage virtual phone number capable of sending SMS.
- (Optional but recommended for exposing your local server during development) ngrok.
- (Optional) Vonage CLI installed (
npm install -g @vonage/cli
).
By the end of this guide, you will have a functional Express application capable of scheduling and sending SMS messages via the Vonage API.
1. Setting up the Project
Let's start by creating our project structure and installing the necessary dependencies.
-
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
-
Initialize Node.js Project: Initialize the project using npm. This creates a
package.json
file.npm init -y
-
Install Dependencies: We need Express for the web server, the Vonage Server SDK for interacting with the API,
node-cron
for scheduling tasks,dotenv
for managing environment variables,express-validator
for input validation, anduuid
for generating unique job IDs.npm install express @vonage/server-sdk node-cron dotenv express-validator uuid
We also need development dependencies for testing:
npm install --save-dev jest supertest
-
Set up Project Structure: Create the basic files and folders.
touch server.js .env .gitignore
server.js
: Our main application code..env
: Stores sensitive credentials and configuration (API keys, phone numbers). Never commit this file to version control..gitignore
: Specifies files and folders that Git should ignore.private.key
: Will be generated/downloaded in Step 6.
-
Configure
.gitignore
: Addnode_modules
and.env
to your.gitignore
file to prevent committing them. It's also crucial to ignore the private key file.node_modules .env private.key
-
Create a Vonage Application: A Vonage application acts as a container for your communication settings and credentials. You need one to use the Messages API with authentication via a private key.
-
Option A: Using the Vonage Dashboard (Recommended)
- Navigate to your Vonage Dashboard.
- Go to ""Applications"" > ""Create a new application"".
- Give your application a name (e.g., ""Node SMS Scheduler"").
- Click ""Generate public and private key"". Save the
private.key
file that downloads immediately. Move this file into your project's root directory. Treat this key like a password – keep it secure and do not commit it. - Enable the ""Messages"" capability.
- For ""Inbound URL"" and ""Status URL"", you can initially enter placeholder URLs like
https://example.com/webhooks/inbound
andhttps://example.com/webhooks/status
. These are only needed if you plan to handle incoming messages or delivery receipts, which are not covered in this core guide. - Click ""Create application"".
- Note down the Application ID displayed on the next page.
-
Option B: Using the Vonage CLI If you installed the CLI and configured it (
vonage config:set --apiKey=YOUR_API_KEY --apiSecret=YOUR_API_SECRET
), you can use:vonage apps:create ""Node SMS Scheduler"" --messages_inbound_url=https://example.com/webhooks/inbound --messages_status_url=https://example.com/webhooks/status --keyfile=private.key
This command creates the app, generates
private.key
in your current directory, and outputs the Application ID.
-
-
Purchase and Link a Vonage Number: You need a Vonage virtual number to send SMS from.
-
Option A: Using the Dashboard
- Go to ""Numbers"" > ""Buy numbers"".
- Search for a number with SMS capability in your desired country.
- Buy the number.
- Go back to ""Applications"", find your ""Node SMS Scheduler"" application, and click its name.
- Click ""Link"" next to the number you just purchased.
-
Option B: Using the Vonage CLI Search for a number (e.g., in the US):
vonage numbers:search US --features=SMS
Buy one of the available numbers:
vonage numbers:buy YOUR_CHOSEN_NUMBER US
Link the number to your application (replace
YOUR_APP_ID
andYOUR_VONAGE_NUMBER
):vonage apps:link --number=YOUR_VONAGE_NUMBER YOUR_APP_ID
-
-
Configure Environment Variables: Open the
.env
file and add your credentials and configuration. Replace the placeholder values with your actual data.# Vonage Credentials VONAGE_API_KEY=""YOUR_API_KEY"" # Found on Vonage Dashboard homepage VONAGE_API_SECRET=""YOUR_API_SECRET"" # Found on Vonage Dashboard homepage VONAGE_APPLICATION_ID=""YOUR_APPLICATION_ID"" # From Step 6 VONAGE_PRIVATE_KEY_PATH=""./private.key"" # Path to the downloaded private key file VONAGE_NUMBER=""YOUR_VONAGE_NUMBER"" # The Vonage number you linked in Step 7 # Application Settings PORT=3000 # Port the server will listen on CRON_TIMEZONE=""America/New_York"" # Default timezone for scheduling (See https://momentjs.com/timezone/)
VONAGE_API_KEY
,VONAGE_API_SECRET
: Used by the Vonage CLI (if used for setup) and potentially for other Vonage API interactions (like number management). The Messages API primarily uses Application ID/Private Key for authentication in this example.VONAGE_APPLICATION_ID
,VONAGE_PRIVATE_KEY_PATH
: Essential for authenticating Messages API calls securely.VONAGE_NUMBER
: The 'from' number for outgoing SMS.PORT
: Local development port.CRON_TIMEZONE
: Crucial for ensuring jobs run at the correct time relative to the user's expectation. Set this to your primary target timezone or allow users to specify it.
Now our project is set up with the necessary structure, dependencies, and configuration.
2. Implementing Core Functionality: Scheduling and Sending
We'll now write the code in server.js
to set up the Express server, initialize the Vonage SDK, and implement the core scheduling logic using node-cron
.
// server.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const cron = require('node-cron');
const { body, validationResult } = require('express-validator');
const { v4: uuidv4 } = require('uuid'); // For generating unique job IDs
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 request bodies
// --- Vonage SDK Initialization ---
// We use Application ID and Private Key for secure authentication with Messages API
let vonage;
try {
vonage = new Vonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH // Path to your private key file
}, { debug: false }); // Set debug: true for verbose SDK logging
console.log(""Vonage SDK initialized successfully."");
} catch (error) {
console.error(""Error initializing Vonage SDK:"", error.message);
console.error(""Ensure VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH are set correctly in .env and the private key file exists."");
process.exit(1); // Exit if SDK can't initialize
}
// --- In-memory storage for scheduled jobs ---
// WARNING: This is for demonstration only. In production, use a persistent store (database).
// If the server restarts, all jobs stored here will be lost. See Section 6 for discussion.
const scheduledJobs = new Map(); // Stores cron tasks by job ID
// --- Core Scheduling Function ---
function scheduleSms(jobId, targetTime, toNumber, message) {
const cronTime = convertToCron(targetTime);
if (!cron.validate(cronTime)) {
console.error(`[Job ${jobId}] Invalid cron time format generated: ${cronTime} from targetTime: ${targetTime}`);
throw new Error('Invalid target time resulted in invalid cron schedule.');
}
console.log(`[Job ${jobId}] Scheduling SMS to ${toNumber} at ${targetTime} (Cron: ${cronTime})`);
// Schedule the task
const task = cron.schedule(cronTime, async () => {
console.log(`[Job ${jobId}] Triggered! Sending SMS to ${toNumber}`);
try {
const resp = await vonage.messages.send({
message_type: ""text"",
to: toNumber,
from: process.env.VONAGE_NUMBER, // Your Vonage sending number
channel: ""sms"",
text: message
});
console.log(`[Job ${jobId}] SMS potentially sent! Message UUID: ${resp.message_uuid}`);
// Once sent, remove from our in-memory store
scheduledJobs.delete(jobId);
// Note: task.stop() is implicitly called as the job function finishes for a one-off task like this.
// If this were a recurring task, you'd manage its lifecycle more explicitly.
} catch (err) {
console.error(`[Job ${jobId}] Error sending SMS via Vonage:`, err.response ? err.response.data : err.message);
// Implement retry logic or error logging here if needed
// For now, we just log and the job won't run again
scheduledJobs.delete(jobId); // Remove failed job
}
}, {
scheduled: true,
timezone: process.env.CRON_TIMEZONE || ""Etc/UTC"" // Use configured timezone or default to UTC
});
// Store the task so we can potentially manage it later (e.g., cancel)
scheduledJobs.set(jobId, task);
console.log(`[Job ${jobId}] Task scheduled successfully.`);
return task;
}
// --- Helper Function: Convert ISO DateTime to Cron String ---
// Example: ""2025-04-20T15:30:00Z"" -> ""30 15 20 4 *"" (Minute Hour DayOfMonth Month *)
// Note: This is a basic conversion. For robust parsing, consider libraries like 'moment-timezone'.
function convertToCron(isoDateTimeString) {
try {
const date = new Date(isoDateTimeString);
if (isNaN(date.getTime())) {
throw new Error('Invalid date string provided');
}
const minute = date.getMinutes();
const hour = date.getHours();
const dayOfMonth = date.getDate();
const month = date.getMonth() + 1; // Cron months are 1-12, JS Date months are 0-11
// Day of week is not needed for a specific date/time schedule
return `${minute} ${hour} ${dayOfMonth} ${month} *`;
} catch (e) {
console.error(`Error converting date string ""${isoDateTimeString}"" to cron format:`, e.message);
throw e; // Re-throw to be caught by the caller
}
}
// --- Basic Health Check Endpoint ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// Add API Endpoint for scheduling (defined in Section 3)
// Add Central Error Handling Middleware (defined in Section 5)
// --- Start Server ---
const server = app.listen(port, () => { // Store server instance for potential graceful shutdown needs
console.log(`SMS Scheduler listening on http://localhost:${port}`);
console.log(`Default scheduling timezone: ${process.env.CRON_TIMEZONE || ""Etc/UTC""}`);
console.log(`Current server time: ${new Date().toString()}`);
});
// Export the app instance for testing purposes
module.exports = app;
// --- Graceful Shutdown ---
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
function shutdown() {
console.log('Shutting down gracefully...');
// Stop all cron jobs held in the in-memory map.
// Note: If using a database-backed worker approach (Section 6),
// the shutdown logic might focus on stopping the worker from picking up new jobs
// or use a different mechanism, rather than managing individual cron tasks here.
scheduledJobs.forEach((task, jobId) => {
console.log(`Stopping job ${jobId}`);
task.stop();
});
// Close the server (optional, helps release port quickly)
server.close(() => {
console.log('HTTP server closed.');
// Perform any other cleanup
console.log('Shutdown complete.');
process.exit(0);
});
// Force exit after a timeout if server.close() hangs
setTimeout(() => {
console.error('Could not close connections in time, forcing shutdown');
process.exit(1);
}, 10000); // 10 seconds timeout
}
Explanation:
- Dependencies: We import
express
,Vonage
,node-cron
,express-validator
, anduuid
.dotenv.config()
loads the.env
file. - Express App: A standard Express app instance is created.
- Middleware:
express.json()
andexpress.urlencoded()
are essential for parsing incoming request bodies. - Vonage SDK Init: We initialize the
Vonage
SDK using theapplicationId
andprivateKey
path from our environment variables. This method is preferred for the Messages API. Error handling is added to ensure the app exits if the SDK fails to load (likely due to missing config). - In-Memory Storage (
scheduledJobs
): AMap
is used to hold references to the activenode-cron
tasks, keyed by a unique job ID. This is explicitly not production-ready. If the server restarts, all scheduled jobs are lost. A database (like Redis, PostgreSQL, or MongoDB) is needed for persistence (see Section 6). scheduleSms
Function:- Takes a unique
jobId
, the target time (targetTime
as an ISO 8601 string like2025-04-20T10:30:00Z
), the recipient number (toNumber
), and themessage
text. - Calls
convertToCron
to translate the ISO time into anode-cron
compatible string. - Uses
cron.validate
to check the generated cron string. - Calls
cron.schedule
with the cron string, the asynchronous callback function to execute, and options (scheduled: true
to start immediately,timezone
). - The callback function logs the trigger, then uses
vonage.messages.send()
to send the SMS via the Messages API.message_type: ""text""
specifies an SMS.to
,from
,channel
, andtext
are provided.
- Basic success and error logging is included within the callback. Upon completion (success or error), the job is removed from the
scheduledJobs
map. - The
cron.schedule
function returns a task object, which we store in ourscheduledJobs
map.
- Takes a unique
convertToCron
Helper: A simple function to parse an ISO 8601 date string and format it into anode-cron
string for a specific date and time. It includes basic error handling for invalid date strings.- Health Check: A simple
/health
endpoint is good practice for monitoring. - Server Start: Starts the Express server on the configured port. We store the server instance returned by
app.listen
. - Module Export:
module.exports = app;
is added to allow importing the Express app in test files. - Graceful Shutdown: Listens for
SIGTERM
(common signal for shutdown) andSIGINT
(Ctrl+C) to attempt stopping active cron jobs (from the in-memory map) and closing the HTTP server before exiting. Includes a timeout. A note is added regarding how this might differ with a database approach.
3. Building the API Layer
Now, let's create the API endpoint that clients will use to submit scheduling requests.
Add the following code inside server.js
, before the app.listen
call and after the helper functions (scheduleSms
, convertToCron
):
// server.js (continued - place before app.listen)
// --- API Endpoint: Schedule SMS ---
app.post('/schedule',
// Input validation rules
[
body('toNumber').isMobilePhone('any', { strictMode: false }).withMessage('Valid E.164 phone number required for toNumber'),
body('message').isString().trim().isLength({ min: 1, max: 1600 }).withMessage('Message is required (max 1600 chars)'),
body('sendAt').isISO8601().withMessage('sendAt must be a valid ISO 8601 date-time string (e.g., 2025-12-31T23:59:59Z)'),
// Optional: Validate sendAt is in the future
body('sendAt').custom(value => {
if (new Date(value) <= new Date()) {
throw new Error('sendAt must be a future date and time');
}
return true;
})
]_
(req_ res_ next) // Add 'next' for error handling middleware
=> {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.warn(""Validation errors:"", errors.array());
return res.status(400).json({ errors: errors.array() });
}
const { toNumber, message, sendAt } = req.body;
const jobId = uuidv4(); // Generate a unique ID for this job
try {
console.log(`Received schedule request ${jobId}: To ${toNumber} at ${sendAt}`);
scheduleSms(jobId, sendAt, toNumber, message);
// Respond immediately to the client
res.status(202).json({ // 202 Accepted: Request received, processing will happen later
jobId: jobId,
status: 'Scheduled',
sendAt: sendAt,
toNumber: toNumber,
message: `SMS scheduled. Will attempt to send around ${sendAt}.`
});
} catch (error) {
// Catch errors from scheduleSms (e.g., invalid cron format from bad date)
console.error(`[Job ${jobId}] Failed to schedule SMS:`, error.message);
// Pass error to the central error handler
next(error); // Forward the error
}
}
);
// Add Central Error Handling Middleware (defined in Section 5)
// --- Start Server --- (app.listen call remains after this)
// --- Export app --- (module.exports = app; remains after this)
// --- Graceful Shutdown --- (remains after this)
Explanation:
- Route Definition: We define a
POST
route at/schedule
. - Input Validation (
express-validator
):- An array of validation rules is passed as middleware before our main route handler.
body('toNumber').isMobilePhone()
: Checks iftoNumber
looks like a valid phone number (accepts various formats including E.164 like+14155552671
).body('message').isString()...
: Ensuresmessage
is a non-empty string within reasonable length limits.body('sendAt').isISO8601()
: Validates thatsendAt
is a standard date-time string. UTC (Z
suffix or offset) is highly recommended for clarity.body('sendAt').custom()
: Adds a custom check to ensure thesendAt
date is in the future.
- Validation Check: Inside the route handler,
validationResult(req)
collects any errors. If errors exist, a400 Bad Request
response is sent with details. - Extract Data & Generate ID: If validation passes, we extract
toNumber
,message
, andsendAt
from the request body. A uniquejobId
is generated usinguuidv4
. - Call
scheduleSms
: We pass the data to our corescheduleSms
function. - Respond
202 Accepted
: We immediately send a202 Accepted
response back to the client. This is appropriate because the SMS sending happens later. The response includes thejobId
which could potentially be used to check status or cancel the job (if those features were implemented). - Error Handling: A
try...catch
block wraps thescheduleSms
call. IfscheduleSms
throws an error (e.g., fromconvertToCron
), we catch it, log it, and pass it to thenext
function, which will forward it to our error handling middleware (to be added next).
Testing the Endpoint:
You can test this endpoint using curl
or a tool like Postman.
Start your server:
node server.js
Send a request using curl
(replace placeholders):
curl -X POST http://localhost:3000/schedule \
-H ""Content-Type: application/json"" \
-d '{
""toNumber"": ""+14155550100"",
""message"": ""Hello from the Node.js Scheduler! This is a test."",
""sendAt"": ""2025-04-20T18:00:00Z""
}'
(Adjust the sendAt
time to be a few minutes in the future from your current time, ensuring it's in ISO 8601 format, preferably UTC indicated by 'Z').
You should receive a 202 Accepted
response:
{
""jobId"": ""some-uuid-string"",
""status"": ""Scheduled"",
""sendAt"": ""2025-04-20T18:00:00Z"",
""toNumber"": ""+14155550100"",
""message"": ""SMS scheduled. Will attempt to send around 2025-04-20T18:00:00Z.""
}
Check your server console logs for scheduling confirmation. At the specified sendAt
time, you should see logs indicating the job triggered and attempted to send the SMS via Vonage. The SMS should arrive on the toNumber
phone shortly after.
4. Integrating with Vonage (Covered in Setup)
Section 1 already covered the essential Vonage integration steps:
- Account Creation: Signing up for Vonage.
- API Credentials: Obtaining the API Key and Secret.
- Application Creation: Creating a Vonage Application, generating the Application ID and Private Key.
- Number Purchase & Linking: Acquiring a Vonage number and linking it to the application.
- Environment Variables: Securely storing all credentials (
VONAGE_API_KEY
,VONAGE_API_SECRET
,VONAGE_APPLICATION_ID
,VONAGE_PRIVATE_KEY_PATH
,VONAGE_NUMBER
) in the.env
file. - SDK Initialization: Using the Application ID and Private Key path to initialize the Vonage SDK in
server.js
.
Secure Handling of Credentials:
- The
.env
file keeps secrets out of your codebase. - The
.gitignore
file prevents accidental commits of the.env
file and theprivate.key
. - In production environments, use your hosting provider's mechanism for managing environment variables securely (e.g., AWS Secrets Manager, Azure Key Vault, Heroku Config Vars, Docker secrets). Do not deploy
.env
files directly to production servers. Ensure theprivate.key
file content is also handled securely.
5. Implementing Error Handling and Logging
Robust error handling and clear logging are crucial for production systems.
-
Centralized Error Handling Middleware: Add a final middleware in
server.js
after all your routes but beforeapp.listen
. This catches errors passed vianext(error)
.// server.js (continued - place after API routes, before app.listen) // --- API Endpoint: Schedule SMS --- (defined above) // --- Central Error Handling Middleware --- // This MUST be the last 'app.use' before 'app.listen' app.use((err, req, res, next) => { console.error(""Unhandled Error:"", err.stack || err.message); // Log the full error stack // Determine appropriate status code // You might want more sophisticated logic based on error type const statusCode = err.statusCode || 500; // Use error's status code or default to 500 res.status(statusCode).json({ status: 'error', statusCode: statusCode, message: err.message || 'An internal server error occurred.' // In development, you might want to include the stack trace: // stack: process.env.NODE_ENV === 'development' ? err.stack : undefined }); }); // --- Start Server --- (app.listen call remains after this) // --- Export app --- (module.exports = app; remains after this) // --- Graceful Shutdown --- (remains after this)
Now, if
scheduleSms
throws an error and callsnext(error)
, this middleware will catch it and return a standardized JSON error response. -
Enhanced Logging: While
console.log
is fine for development, production requires more structured logging. Consider libraries like:- Winston: Highly configurable, multiple transports (console, file, databases).
- Pino: Focuses on high performance, low overhead JSON logging.
Example using console logging levels (basic improvement):
// Replace simple console.log with levels for better filtering later console.info(""INFO: Server starting...""); // Informational console.warn(""WARN: Validation failed for request...""); // Warnings console.error(""ERROR: Failed to send SMS:"", err); // Errors // In scheduleSms function // Replace console.log(`[Job ${jobId}] Scheduling...`) with: console.info(`[Job ${jobId}] INFO: Scheduling SMS to ${toNumber} at ${targetTime}`); // Replace console.error(`[Job ${jobId}] Error sending SMS...`) with: console.error(`[Job ${jobId}] ERROR: Error sending SMS via Vonage:`, err.response ? err.response.data : err.message);
-
Vonage API Error Handling: The
try...catch
block aroundvonage.messages.send()
already catches immediate API errors (like auth failures, invalid numbers detected by Vonage before sending). Check theerr.response.data
for detailed error information from Vonage when available. -
Retry Mechanisms:
- Scheduling Failures: If
scheduleSms
fails beforecron.schedule
is called (e.g., badsendAt
format), the API endpoint returns an error, and the client should retry. - SMS Sending Failures: If
vonage.messages.send()
fails inside the cron job callback:- Simple: Log the error. The job won't run again (as implemented). Manual intervention might be needed.
- Advanced: Implement a retry strategy within the callback. Use libraries like
async-retry
orp-retry
. Apply exponential backoff (wait longer between retries) to avoid overwhelming the API. Store retry attempts/status in your persistent store (database). This adds significant complexity. For this guide, we stick to logging the failure.
- Scheduling Failures: If
6. Database Schema and Data Layer (Improvement Suggestion)
The current in-memory scheduledJobs
map is not suitable for production as data is lost on restart. A database is required for persistence and scalability.
Conceptual Schema (e.g., PostgreSQL):
CREATE TABLE scheduled_sms_jobs (
job_id UUID PRIMARY KEY, -- Unique identifier
to_number VARCHAR(20) NOT NULL, -- Recipient phone number (E.164 format recommended)
message TEXT NOT NULL, -- SMS message content
send_at TIMESTAMPTZ NOT NULL, -- Scheduled time (with timezone is crucial)
status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, PROCESSING, SENT, FAILED, CANCELED
vonage_message_uuid VARCHAR(50) NULL, -- UUID from Vonage API on success
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
last_attempt_at TIMESTAMPTZ NULL,
error_message TEXT NULL, -- Store error details if failed
retry_count INT DEFAULT 0 -- For implementing retries
);
-- Index for querying pending jobs efficiently by the worker
CREATE INDEX idx_pending_jobs ON scheduled_sms_jobs (status, send_at)
WHERE status = 'PENDING';
-- Optional index for looking up jobs by ID (if needed for status checks/cancellation)
CREATE INDEX idx_job_lookup ON scheduled_sms_jobs (job_id);
Data Layer Implementation Strategy:
- Use an ORM (like Sequelize, TypeORM, Prisma) or a database client library (like
pg
for PostgreSQL,mysql2
for MySQL,mongodb
for MongoDB). - Modify the
/schedule
endpoint handler:- Validate input.
- Insert a new record into
scheduled_sms_jobs
withstatus = 'PENDING'
,job_id
,to_number
,message
,send_at
. - Return
201 Created
or202 Accepted
with thejob_id
.
- Create a separate background worker process OR modify the
cron
logic:- Instead of scheduling individual
node-cron
jobs per request, have one recurringnode-cron
job (e.g., runs every minute or more frequently). - This job queries the database for
scheduled_sms_jobs
wherestatus = 'PENDING'
andsend_at <= NOW()
. UseSELECT ... FOR UPDATE SKIP LOCKED
or similar transaction locking to prevent multiple workers grabbing the same job. - For each due job retrieved:
- Update its
status
toPROCESSING
. - Attempt to send the SMS using
vonage.messages.send()
. - Update the job record's
status
toSENT
(and storevonage_message_uuid
) orFAILED
(and storeerror_message
). Incrementretry_count
if implementing retries. Handle potential database update failures. - Commit the transaction.
- Update its
- Instead of scheduling individual
This database-backed approach is far more robust, scalable, and resilient to restarts. Implementing it fully is beyond this initial guide but is the strongly recommended next step for production use.
7. Adding Security Features
Basic security measures are essential.
-
Input Validation: Already implemented using
express-validator
in the/schedule
endpoint. This prevents malformed data and potential injection issues. -
Environment Variable Security: Covered in Section 4 – keep
.env
out of Git, use secure mechanisms in production for credentials and theprivate.key
content. -
Rate Limiting: Protect your API from abuse and brute-force attacks. Use middleware like
express-rate-limit
.Install:
npm install express-rate-limit
Implement in
server.js
(near the top, after basic middleware likeexpress.json
):// server.js (continued - place near the top) const rateLimit = require('express-rate-limit'); // --- Middleware --- app.use(express.json()); app.use(express.urlencoded({ extended: true })); // --- Rate Limiter --- const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers message: 'Too many requests from this IP, please try again after 15 minutes' }); // Apply the rate limiting middleware to all requests or specific endpoints // Apply globally: app.use(limiter); // Or apply specifically to the schedule endpoint: // app.use('/schedule', limiter); // --- Vonage SDK Initialization --- (remains here)