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 set up the project, schedule messages using node-cron
, send SMS via Vonage, handle API interactions securely, and deploy the application.
This application solves the common need to send timely SMS notifications or reminders without manual intervention – useful for appointment confirmations, event reminders, automated alerts, or marketing campaigns scheduled in advance.
Technologies Used:
- Node.js: A JavaScript runtime environment for building the backend server.
- Express: A minimal and flexible Node.js web application framework used to create the API endpoint.
- Vonage Messages API: A powerful API for sending and receiving messages across various channels, including SMS. We'll use the
@vonage/server-sdk
Node.js library. node-cron
: A simple cron-like job scheduler for Node.js, used to trigger SMS sending at specific times.dotenv
: A module to load environment variables from a.env
file, keeping sensitive credentials secure.
System Architecture:
+-------------+ +----------------------+ +-------------------+ +-----------------+
| User/Client | ----> | Node.js/Express API | ----> | node-cron Scheduler | ----> | Vonage Messages | --(SMS)--> User Phone
| (e.g., curl)| | (POST /schedule-sms) | | (In-memory jobs) | | API |
+-------------+ +----------------------+ +-------------------+ +-----------------+
| | |
+----------------------+--------------------------------------------------------+
| (Credentials, Configuration)
v
+-----------+
| .env File |
+-----------+
Prerequisites:
- A Vonage API account (Sign up for free credit).
- Your Vonage API Key and API Secret (found on the Vonage API Dashboard). Note: While Key/Secret are available, this guide primarily uses Application ID + Private Key authentication for the Messages API.
- Node.js and npm (or yarn) installed (Download Node.js).
- A Vonage virtual phone number capable of sending SMS (Buy Numbers).
- (Optional but Recommended) Vonage CLI installed (
npm install -g @vonage/cli
). - (Optional) ngrok: Useful if you plan to extend this application later to handle inbound SMS messages via webhooks, as it exposes your local server to the internet. Not required for the sending-only functionality described in this guide.
Final Outcome:
By the end of this guide, you will have a running Node.js application with a single API endpoint (/schedule-sms
). Sending a POST request to this endpoint with a recipient number, message text, and a future timestamp will schedule an SMS message to be sent automatically at the specified time via the Vonage Messages API.
1. Setting Up the Project
Let's initialize the project, install dependencies, and configure the Vonage integration.
1.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
1.2 Initialize Node.js Project:
Initialize the project using npm. This creates a package.json
file.
npm init -y
1.3 Install Dependencies:
Install the necessary Node.js packages:
npm install express @vonage/server-sdk node-cron dotenv helmet express-rate-limit
express
: Web framework for the API.@vonage/server-sdk
: The official Vonage Node.js library for interacting with Vonage APIs.node-cron
: For scheduling the SMS sending jobs.dotenv
: To load environment variables from a.env
file.helmet
: For setting various security-related HTTP headers. (Added for Section 7)express-rate-limit
: For basic API rate limiting. (Added for Section 7)
1.4 Set Up Vonage Application and Link Number:
You need a Vonage Application to authenticate API requests and a linked number to send SMS from.
-
Using Vonage CLI (Recommended):
- Configure the CLI with your API key and secret (used for CLI authentication, not necessarily for the app itself):
# Replace with your actual key and secret before running vonage config:set --apiKey=YOUR_API_KEY --apiSecret=YOUR_API_SECRET
- Create a Vonage application. We don't need webhooks for sending scheduled messages, so answer 'No' when asked about creating them. You will need the generated private key.
# Ensure you replace the placeholders below before running vonage apps:create --name="SMS Scheduler App" # Follow prompts: Select Messages capability, choose 'No' for webhooks. # Note the Application ID generated. # It will prompt to download a private key (e.g., private.key). Save this file in your project root.
- Link your Vonage number to the application (replace with your number and the App ID from the previous step):
# Example: vonage apps:link --number=14155550100 aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee # Ensure you replace the placeholders below with your actual number and App ID before running vonage apps:link --number=YOUR_VONAGE_NUMBER YOUR_APPLICATION_ID
- Configure the CLI with your API key and secret (used for CLI authentication, not necessarily for the app itself):
-
Using Vonage Dashboard (Alternative):
- Go to Your Applications.
- Click "Create a new application".
- Give it a name (e.g., "SMS Scheduler App").
- Click "Generate public and private key". Save the
private.key
file that downloads – place it in your project's root directory. Note the Application ID. - Enable the "Messages" capability. You can leave the webhook URLs blank for now as we are only sending messages.
- Click "Generate new application".
- Go to the "Link numbers" section on the application page and link your purchased Vonage number.
1.5 Configure Environment Variables:
Create a file named .env
in the root of your project directory. This file will store your sensitive credentials and configuration. Never commit this file to version control. Add a .gitignore
file with .env
and private.key
listed.
# .env
# Vonage Credentials (using Application ID and Private Key for Messages API)
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
# Path relative to project root for the private key downloaded in step 1.4
VONAGE_PRIVATE_KEY_PATH=./private.key
# Your Vonage virtual number in E.164 format (e.g., 14155550100)
VONAGE_FROM_NUMBER=YOUR_VONAGE_NUMBER
# Optional: API Key/Secret might be needed for other Vonage APIs or CLI setup
# VONAGE_API_KEY=YOUR_VONAGE_API_KEY
# VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
# Application Settings
PORT=3000 # Port the Express server will run on
Replace the placeholder values with your actual Vonage Application ID, private key filename (if different), and Vonage number.
1.6 Project Structure (Example):
Your project directory should now look something like this:
vonage-sms-scheduler/
├── .env
├── .gitignore
├── node_modules/
├── package.json
├── package-lock.json
├── private.key
└── server.js (We will create this next)
Add .env
, node_modules/
, and private.key
to your .gitignore
file:
# .gitignore
node_modules
.env
private.key
2. Implementing Core Functionality
Now, let's write the code for the Express server, Vonage SDK initialization, and the scheduling logic.
2.1 Create server.js
:
Create a file named server.js
in your project root. This will contain the main application logic.
2.2 Basic Express Server Setup:
Add the following boilerplate code to server.js
to set up Express and load environment variables:
// server.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const cron = require('node-cron');
const { Vonage } = require('@vonage/server-sdk');
const { Auth } = require('@vonage/auth');
const path = require('path');
const fs = require('fs');
const helmet = require('helmet'); // Added for security headers
const rateLimit = require('express-rate-limit'); // Added for rate limiting
const app = express();
const port = process.env.PORT || 3000;
// --- Security Middleware ---
app.use(helmet()); // Apply basic security headers
// Middleware to parse JSON request bodies
app.use(express.json());
// Basic route for testing server is running
app.get('/', (req, res) => {
res.send('SMS Scheduler API is running!');
});
// Start the server (will be moved after routes/middleware)
// app.listen(port, () => {
// console.log(`Server listening at http://localhost:${port}`);
// });
2.3 Initialize Vonage SDK:
Inside server.js
, after loading dependencies but before defining routes, initialize the Vonage SDK using your credentials from the .env
file. We read the private key file content directly.
// server.js (Add this section after require statements)
// --- Vonage SDK Initialization ---
let vonage;
let privateKey;
try {
// Construct the absolute path to the private key
const privateKeyPath = path.resolve(__dirname, process.env.VONAGE_PRIVATE_KEY_PATH);
// Check if the private key file exists
if (!fs.existsSync(privateKeyPath)) {
throw new Error(`Private key file not found at path: ${privateKeyPath}. Please ensure VONAGE_PRIVATE_KEY_PATH in .env is correct.`);
}
privateKey = fs.readFileSync(privateKeyPath);
// Validate essential environment variables for App ID + Private Key auth
if (!process.env.VONAGE_APPLICATION_ID || !privateKey || !process.env.VONAGE_FROM_NUMBER) {
throw new Error('Missing required Vonage environment variables (Application ID, Private Key Path, From Number). Check your .env file.');
}
const credentials = new Auth({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKey,
});
vonage = new Vonage(credentials);
console.log('Vonage SDK Initialized Successfully.');
} catch (error) {
console.error('CRITICAL: Error initializing Vonage SDK:', error.message);
// Exit if SDK cannot be initialized, as the app cannot function
process.exit(1);
}
// --- End Vonage SDK Initialization ---
- Why this approach? We use the
Auth
class with the Application ID and the content of the private key. This is the recommended secure method for authenticating with Vonage APIs like the Messages API that require application context. We explicitly check for the file's existence and essential variables for better startup diagnostics. The application exits if initialization fails, as it cannot perform its core function.
2.4 Implement the Scheduling Logic:
We'll use node-cron
to schedule tasks. When the API receives a request, it validates the time, creates a cron
job scheduled for that specific time, and the job's task will be to send the SMS using the Vonage SDK.
// server.js (Add this section)
// --- Scheduling Function ---
function scheduleSms(to, text, scheduleTime) {
const now = new Date();
const scheduledDate = new Date(scheduleTime);
// Basic validation: Ensure the scheduled time is in the future
if (isNaN(scheduledDate.getTime()) || scheduledDate <= now) {
console.error(`Invalid schedule time: ${scheduleTime}. Must be a valid date string in the future.`);
return false; // Indicate scheduling failure
}
// Convert scheduledDate to cron pattern (minute hour dayOfMonth month dayOfWeek)
// Note: node-cron uses system time by default. Be mindful of server timezones.
const minute = scheduledDate.getMinutes();
const hour = scheduledDate.getHours();
const dayOfMonth = scheduledDate.getDate();
const month = scheduledDate.getMonth() + 1; // Cron months are 1-12
// Day of week is not strictly necessary if date is specified_ use '*'
const cronPattern = `${minute} ${hour} ${dayOfMonth} ${month} *`;
console.log(`Scheduling SMS to ${to} at ${scheduledDate.toISOString()} (Cron: ${cronPattern})`);
// Schedule the job
const task = cron.schedule(cronPattern_ async () => {
console.log(`Executing scheduled SMS job for ${to} at ${new Date().toISOString()}`);
try {
if (!vonage) {
// This check might be redundant due to process.exit on init failure, but safe to keep.
console.error('Vonage SDK not initialized. Cannot send SMS.');
task.stop(); // Stop the task if SDK is broken
return;
}
const fromNumber = process.env.VONAGE_FROM_NUMBER;
// Consider adding retry logic here (see Section 5)
const resp = await vonage.messages.send({
message_type: ""text"",
to: to, // Recipient number from the request
from: fromNumber, // Your Vonage number from .env
channel: ""sms"",
text: text, // Message text from the request
});
console.log(`SMS Message sent successfully to ${to}. Message UUID: ${resp.message_uuid}`);
} catch (err) {
// Log detailed error information if available from Vonage response
const errorDetails = err?.response?.data || err.message;
console.error(`Error sending SMS to ${to}:`, JSON.stringify(errorDetails, null, 2));
// Implement more robust error handling/retry logic here if needed
} finally {
// Stop the task after execution since it's a one-off schedule
task.stop();
console.log(`Cron task stopped for ${to} at ${scheduledDate.toISOString()}`);
}
}, {
scheduled: true,
// Consider timezone if your server/users are in different zones
// timezone: ""America/New_York""
});
return true; // Indicate scheduling success
}
// --- End Scheduling Function ---
- Why
node-cron
? It's simple for in-process scheduling. For high-volume or distributed systems, a dedicated job queue (like BullMQ with Redis) is generally better. - Cron Pattern: We dynamically generate the cron pattern based on the user-provided time.
- Error Handling: Basic
try...catch
aroundvonage.messages.send
. Production systems need more robust error logging and potentially retry mechanisms. Logging the stringified error details provides more context. - Time Zones:
node-cron
uses the server's system time zone by default. Be explicit with thetimezone
option if needed. JavaScriptDate
objects can also be timezone-sensitive; ensure consistent handling (e.g., always use UTC internally via ISO 8601 'Z' format). - Task Stopping:
task.stop()
ensures the cron job doesn't linger after firing once. Remove this if you intend for the schedule to repeat based on the pattern.
3. Building the API Layer
Let's create the Express endpoint that accepts scheduling requests.
3.1 Create the /schedule-sms
Endpoint:
Add the following POST route handler and associated middleware in server.js
, typically before the app.listen
call:
// server.js (Add this section)
// --- Rate Limiting Middleware (Apply before the route) ---
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests created from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply rate limiter specifically to the scheduling endpoint
app.use('/schedule-sms', apiLimiter);
// --- API Endpoint for Scheduling SMS ---
app.post('/schedule-sms', (req, res) => {
const { to, message, scheduleTime } = req.body;
// 1. Basic Input Validation
if (!to || !message || !scheduleTime) {
return res.status(400).json({
error: 'Missing required fields: to, message, scheduleTime'
});
}
// Basic E.164-like format check (digits, optional leading +)
if (!/^\+?\d{10,15}$/.test(to)) {
return res.status(400).json({ error: 'Invalid phone number format for "to". Use E.164 format (e.g., +14155550123).' });
}
// 2. Schedule the SMS
const scheduled = scheduleSms(to, message, scheduleTime);
// 3. Send Response
if (scheduled) {
res.status(202).json({
message: 'SMS scheduled successfully.',
details: {
to: to,
// Avoid logging potentially sensitive message content in production responses
// message: message,
scheduleTime: new Date(scheduleTime).toISOString() // Echo back standardized time
}
});
} else {
// scheduleSms handles logging the specific error
res.status(400).json({ error: 'Invalid schedule time. Must be a valid ISO 8601 date string in the future.' });
}
});
// --- End API Endpoint ---
// --- Basic Health Check Route ---
app.get('/health', (req, res) => {
// Add more checks here if needed (e.g., DB connection if using one)
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// --- Basic Error Handling Middleware (Add as the LAST app.use() call) ---
// Catch-all for unhandled errors in route handlers
app.use((err, req, res, next) => {
console.error('Unhandled Error:', err.stack || err);
// Avoid leaking stack traces in production responses
res.status(500).json({ error: 'Internal Server Error' });
});
// --- End Error Handling Middleware ---
// --- Start the Server (Should be the final part of the file) ---
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
console.log(`Current Server Time: ${new Date().toISOString()}`);
console.log('Make sure your .env file is configured correctly.');
});
// --- End Start Server ---
- Input Validation: We check for the presence of required fields (
to
,message
,scheduleTime
) and perform an E.164-like regex check on the phone number. Production apps should use more robust validation libraries (e.g.,express-validator
). - Scheduling Call: We call our
scheduleSms
function. - Response: We return a
202 Accepted
status code, indicating the request was accepted for processing (scheduling), but the SMS hasn't been sent yet. We return a400 Bad Request
for validation errors. - Error Middleware: A simple Express error handler catches unexpected errors. It's crucial this is added after all other routes and middleware.
- Rate Limiting & Security: Basic rate limiting (
express-rate-limit
) and security headers (helmet
) are applied.
3.2 Testing the Endpoint:
Once the server is running (node server.js
), you can test the endpoint using curl
or a tool like Postman.
Example curl
Request:
Remember to replace YOUR_RECIPIENT_NUMBER
with a real phone number (in E.164 format, e.g., +14155550123
). Set scheduleTime
to a valid ISO 8601 timestamp a few minutes in the future (use the 'Z' suffix for UTC).
# Example: Schedule an SMS for 5 minutes from now (adjust time as needed)
# On Linux/macOS (ensure output is UTC with 'Z'):
SCHEDULE_TIME=$(date -u -v+5M +'%Y-%m-%dT%H:%M:%SZ')
# Or manually set a future UTC time: SCHEDULE_TIME="2025-04-20T18:30:00Z"
# Ensure YOUR_RECIPIENT_NUMBER is replaced below!
curl -X POST http://localhost:3000/schedule-sms \
-H "Content-Type: application/json" \
-d '{
"to": "YOUR_RECIPIENT_NUMBER", # <-- Replace this with a valid E.164 number!
"message": "Hello from the Vonage Scheduler! This is your reminder."_
"scheduleTime": "'"$SCHEDULE_TIME"'"
}'
# Example Response (202 Accepted):
# {
# "message": "SMS scheduled successfully."_
# "details": {
# "to": "+14155550123"_
# "scheduleTime": "2025-04-20T18:30:00.000Z"
# }
# }
# Example Error Response (400 Bad Request):
# {
# "error": "Missing required fields: to_ message_ scheduleTime"
# }
# or
# {
# "error": "Invalid schedule time. Must be a valid ISO 8601 date string in the future."
# }
# or
# {
# "error": "Invalid phone number format for \"to\". Use E.164 format (e.g._ +14155550123)."
# }
Check your server console logs for scheduling confirmation and execution logs. Verify that the SMS arrives on the recipient's phone at the scheduled time.
4. Integrating with Vonage (Covered in Setup & Core)
This section's core elements were covered during setup and implementation:
- Configuration: Done via
.env
file (Application ID_ Private Key Path_ From Number). - SDK Initialization: Implemented in
server.js
using@vonage/server-sdk
andAuth
. Application exits if this fails. - API Credential Security: Handled by using environment variables and
.gitignore
. - Sending SMS: Implemented within the
scheduleSms
function usingvonage.messages.send
. - Fallback Mechanisms: Not implemented in this basic guide. For production_ consider:
- Retry Logic: Implement exponential backoff for transient network errors or Vonage API issues (e.g._ 5xx errors_ rate limits) when calling
vonage.messages.send
. Libraries likeasync-retry
can help. - Monitoring & Alerting: Set up monitoring (Section 10) to detect failures quickly.
- Retry Logic: Implement exponential backoff for transient network errors or Vonage API issues (e.g._ 5xx errors_ rate limits) when calling
5. Error Handling_ Logging_ and Retries
- Error Handling Strategy:
- Use
try...catch
blocks around critical operations (SDK initialization_ API calls). - Use Express middleware for unhandled route errors.
- Return appropriate HTTP status codes (400 for client errors_ 202 for accepted_ 500 for server errors).
- Provide informative JSON error messages for API clients.
- Critical SDK initialization failure causes the process to exit.
- Use
- Logging:
- Currently using
console.log
andconsole.error
. Enhanced logging inscheduleSms
catch
block to show Vonage error details. - Production: Integrate a dedicated logging library like Winston or Pino for structured logging (JSON format)_ different log levels (info_ warn_ error)_ and configurable outputs (file_ console_ external services like Datadog_ Logstash).
- Log key events: Server start_ SDK initialization success/failure_ schedule request received (with validation outcome)_ SMS scheduled_ SMS job executed_ SMS sent successfully_ SMS sending failed (with detailed error).
- Currently using
- Retry Mechanisms (Conceptual):
- Wrap the
vonage.messages.send
call inside thecron.schedule
callback with a retry function (e.g._ usingasync-retry
). - Retry only on specific_ potentially transient error types (e.g._ network errors_ 5xx server errors from Vonage_ 429 rate limit errors). Do not retry on 4xx client errors (like invalid number format_ insufficient funds - these require intervention).
- Use exponential backoff to avoid overwhelming the API during outages.
- Limit the number of retries. Log failures after exhausting retries.
- Wrap the
// Example conceptual retry logic using async-retry (install: npm install async-retry)
// const retry = require('async-retry');
// Inside the cron.schedule callback_ replace the direct vonage.messages.send call:
/*
await retry(async (bail_ attempt) => {
try {
console.log(`Attempt ${attempt} to send SMS to ${to}`);
const resp = await vonage.messages.send({ // ... message details ... });
console.log(`SMS Message sent successfully to ${to}. Message UUID: ${resp.message_uuid}`);
return resp; // Return success value
} catch (err) {
const status = err?.response?.status;
const errorDetails = err?.response?.data || err.message;
console.error(`Attempt ${attempt} failed for ${to}: Status ${status}`, JSON.stringify(errorDetails, null, 2));
// Don't retry on 4xx errors (except potentially 429 Too Many Requests)
if (status && status >= 400 && status < 500 && status !== 429) {
bail(new Error(`Non-retriable error (${status}) sending SMS to ${to}`));
return; // Exit retry loop
}
// For other errors (network_ 5xx_ 429)_ throw to trigger retry
throw err;
}
}_ {
retries: 3_ // Number of retries (e.g._ 3 attempts total)
factor: 2_ // Exponential backoff factor
minTimeout: 1000_ // Initial delay 1 second
maxTimeout: 10000_ // Max delay 10 seconds
onRetry: (error_ attempt) => {
console.warn(`Retrying SMS send to ${to} (Attempt ${attempt}) due to: ${error.message}`);
}
}).catch(finalErr => {
// This catch block executes if all retries fail
console.error(`All ${finalErr.retries} attempts failed to send SMS to ${to}. Final Error:`, finalErr.message);
// Log persistent failure here
});
*/
6. Database Schema and Data Layer (Optional Enhancement)
The current implementation uses in-memory scheduling via node-cron
. This means scheduled jobs are lost if the server restarts. For persistent scheduling, you need a database.
-
Why a Database?
- Persistence: Schedules survive server restarts and deployments.
- Scalability: Manage a large number of schedules efficiently without consuming excessive server memory.
- Management: Enables listing, updating, or canceling scheduled messages via additional API endpoints (not implemented here).
- State Tracking: Track the status (PENDING, SENT, FAILED) and Vonage message ID for each scheduled SMS.
-
Choice of Database: Relational (PostgreSQL, MySQL) or NoSQL (MongoDB) databases are suitable.
-
Schema Example (e.g., using Prisma ORM with PostgreSQL):
// schema.prisma datasource db { provider = ""postgresql"" url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" } enum SmsStatus { PENDING SENT FAILED } model ScheduledSms { id String @id @default(uuid()) recipient String message String scheduleTime DateTime @db.Timestamp(6) status SmsStatus @default(PENDING) vonageMsgId String? // Store Vonage message ID on success createdAt DateTime @default(now()) @db.Timestamp(6) updatedAt DateTime @updatedAt @db.Timestamp(6) lastAttempt DateTime? @db.Timestamp(6) retryCount Int @default(0) lastError String? // Add index for querying pending jobs @@index([status, scheduleTime]) }
-
Implementation Sketch:
- Setup: Install Prisma (
npm install prisma --save-dev
,npm install @prisma/client
), initialize (npx prisma init
), define the schema, configureDATABASE_URL
in.env
, run migrations (npx prisma migrate dev
). - API Endpoint (
/schedule-sms
): Instead of callingscheduleSms
directly, validate input and then save the schedule details to theScheduledSms
table in the database withstatus: 'PENDING'
. Respond with 201 Created or 202 Accepted. - Worker Process: Create a separate Node.js process (or use a library like
agenda
or a dedicated job queue like BullMQ backed by Redis). This worker periodically queries the database forPENDING
schedules wherescheduleTime
is less than or equal to the current time. - Processing: For each due schedule retrieved by the worker:
- Attempt to send the SMS via Vonage (potentially with retry logic).
- Update the database record: Set
status
toSENT
and storevonageMsgId
on success. On failure, updatestatus
toFAILED
(or back toPENDING
for retry), incrementretryCount
, loglastError
, and potentially updatescheduleTime
for the next retry attempt.
- Setup: Install Prisma (
-
This guide focuses on the simpler in-memory approach. Implementing a database layer significantly increases complexity but is essential for production systems requiring persistence and scalability.
7. Security Features
- Input Validation:
- Basic checks implemented in the
/schedule-sms
endpoint (presence, phone format). - Enhancement: Use libraries like
express-validator
for more robust validation (e.g., checking message length, stricter phone number formats using libraries likelibphonenumber-js
, date validation).
- Basic checks implemented in the
- API Credential Security:
- Handled via
.env
file for Application ID, Private Key Path, and From Number. .gitignore
prevents committing.env
andprivate.key
.- Ensure the server environment restricts read access to these sensitive files.
- Handled via
- Rate Limiting:
- Basic protection against brute-force/DoS attacks on the
/schedule-sms
endpoint usingexpress-rate-limit
(implemented in Section 3.1). Tune limits based on expected usage.
- Basic protection against brute-force/DoS attacks on the
- HTTPS:
- Crucial for production. Always deploy behind a reverse proxy (like Nginx or Caddy) or use a platform (like Heroku, Render, AWS Elastic Beanstalk) that handles TLS termination (provides HTTPS). Do not expose raw HTTP over the internet.
- Security Headers:
- Use the
helmet
middleware (implemented in Section 2.2) to set various security-related HTTP headers (e.g.,X-Frame-Options
,Strict-Transport-Security
,X-Content-Type-Options
).
- Use the
- Authentication/Authorization (If applicable):
- This guide assumes an internally used or trusted API. If exposed publicly or within a multi-user system, protect the
/schedule-sms
endpoint. - Implement authentication (e.g., using API Keys passed in headers, JWT tokens) to verify the identity of the client making the request.
- Implement authorization to ensure the authenticated client has permission to schedule SMS messages.
- This guide assumes an internally used or trusted API. If exposed publicly or within a multi-user system, protect the
8. Handling Special Cases
- Time Zones:
- Problem:
new Date(string)
parsing andnode-cron
's default behavior depend on the server's system time zone. If the client submittingscheduleTime
is in a different timezone, or if your servers run in UTC (common practice), inconsistencies can arise. - Solution:
- Standardize on UTC: This is the most robust approach. Require clients to send
scheduleTime
in ISO 8601 format with the UTC 'Z' designator (e.g.,2025-04-20T18:30:00Z
). Perform all date comparisons and scheduling relative to UTC. Thenew Date()
constructor in Node.js handles ISO 8601 strings (including the 'Z') correctly.node-cron
uses the system time, so ensure your server system time is reliable (ideally synced via NTP). - Explicit Timezone (More Complex): If user-local time must be supported, require the client to send the IANA timezone identifier (e.g.,
America/New_York
) along with the local time string. Use libraries likedate-fns-tz
orluxon
for reliable timezone conversions on the server. Pass the correcttimezone
option tocron.schedule
. This adds significant complexity.
- Standardize on UTC: This is the most robust approach. Require clients to send
- Problem:
- Invalid Phone Numbers: Vonage API performs its own validation and will return an error (e.g., code
4
- Invalid recipient) for numbers it cannot deliver to. The basic regex check helps catch some format errors early, but rely on Vonage's response for definitive validation. Ensure error handling logs these specific failures clearly. - Message Encoding/Length: Standard SMS (GSM-7 encoding) allows 160 characters. Non-standard characters (like many emojis or non-Latin alphabets) switch encoding to UCS-2, reducing the limit to 70 characters per segment. Longer messages are automatically split into multiple segments by carriers (concatenated SMS) and are typically billed per segment by Vonage. The Vonage Messages API handles this segmentation transparently. Be aware of potential costs for long messages or messages with special characters.