This guide provides a comprehensive, step-by-step walkthrough for building an application that schedules and sends SMS reminders using a Node.js backend, a React frontend built with Vite, and the Vonage Messages API. We'll cover everything from initial project setup to deployment considerations, focusing on creating a robust and maintainable solution.
By the end of this tutorial, you will have a functional web application where users can input a phone number, a message, and a future date/time, scheduling an SMS to be sent via Vonage. We'll address common challenges like environment management, asynchronous job handling, error logging, and basic security. Please note: The core scheduling mechanism used in this example (node-cron
with in-memory storage) is suitable for demonstration but is not inherently production-ready, as explained in the caveats section.
Project Overview and Goals
What We're Building:
- A React frontend (built with Vite) allowing users to submit a phone number, message content, and a specific date/time for sending an SMS.
- A Node.js (Express) backend API that receives scheduling requests from the frontend.
- Integration with the Vonage Messages API to send the scheduled SMS messages.
- A scheduling mechanism (
node-cron
) within the backend to trigger SMS sending at the specified time.
Problem Solved:
This application provides a simple interface for scheduling one-off SMS notifications or reminders, a common requirement for appointment confirmations, event reminders, follow-ups, and more.
Technologies Used:
- Node.js: A JavaScript runtime for building the backend server.
- Express: A minimal and flexible Node.js web application framework for creating the API.
- Vite: A modern frontend build tool providing a fast development experience.
- React: A popular JavaScript library for building user interfaces.
- Vonage Messages API: A powerful API for sending messages across various channels, including SMS. We'll use the
@vonage/server-sdk
. node-cron
: A simple task scheduler for Node.js, based on cron syntax. We'll use this for triggering the SMS sends. (Caveat: See Troubleshooting and Caveats for production considerations).dotenv
: To manage environment variables securely.cors
: To enable Cross-Origin Resource Sharing between the frontend and backend during development.axios
: For making HTTP requests from the frontend.
System Architecture:
graph TD
A[User's Browser (React Frontend)] -- HTTP POST Request --> B(Node.js/Express Backend API);
B -- Schedule Job --> C{node-cron Scheduler};
C -- At Scheduled Time --> D(Vonage Send SMS Logic);
D -- API Call --> E(Vonage Messages API);
E -- Sends SMS --> F(Recipient's Phone);
B -- HTTP 200 OK / Error --> A;
Diagram Description: The user interacts with the React frontend in their browser. Submitting the form sends an HTTP POST request to the Node.js/Express backend API. The backend schedules the job using the node-cron scheduler. At the designated time, the scheduler triggers the Vonage Send SMS logic, which makes an API call to the Vonage Messages API. Vonage then sends the SMS to the recipient's phone. The backend sends an HTTP response (OK or Error) back to the frontend.
Prerequisites:
- Node.js and npm (or yarn): Installed on your system (LTS version recommended). Download from nodejs.org.
- Vonage API Account: Sign up at Vonage API Dashboard. You'll need your API Key, API Secret, and Application ID.
- Vonage Phone Number: Purchase an SMS-capable virtual number through the Vonage Dashboard.
- Basic understanding: JavaScript, Node.js, React, REST APIs, and terminal/command line usage.
- Code Editor: Such as VS Code.
- (Optional) Vonage CLI: Can be useful for managing applications and numbers. Install via
npm install -g @vonage/cli
. - (Optional) Git: For version control.
Final Outcome:
A two-part application (frontend and backend) running locally. The frontend presents a form, and upon submission, the backend schedules an SMS via Vonage using node-cron
.
1. Setting Up the Vonage Account and Application
Before writing code, configure your Vonage account and create a Messages API application.
1.1. Create a Vonage Account:
- Go to the Vonage API Dashboard and create an account if you don't have one.
- Note your API Key and API Secret found on the dashboard homepage. Keep these secure.
1.2. Purchase a Vonage Number:
- Navigate to "Numbers" > "Buy numbers" in the dashboard.
- Search for a number with SMS capability in your desired country.
- Purchase the number. Note this number down – it will be your
VONAGE_FROM_NUMBER
.
1.3. Create a Vonage Application:
Vonage Applications act as containers for your communication settings and credentials.
- Navigate to "Applications" > "Create a new application".
- Name: Give your application a descriptive name (e.g., "Node SMS Scheduler").
- Capabilities: Toggle on "Messages".
- Inbound URL: Enter
http://localhost:5000/webhooks/inbound
(Even if not used for receiving in this guide, it's often required). - Status URL: Enter
http://localhost:5000/webhooks/status
(This receives delivery receipts). - (Note: These URLs are placeholders for local development. For production, they'd point to your deployed backend's public URL. Also note that while the Vonage setup asks for the full URL here, our backend code implements these specific webhook paths directly at the server root, not prefixed under
/api
like the scheduling endpoint.)
- Inbound URL: Enter
- Generate Public and Private Key: Click this button. A
private.key
file will be downloaded. Save this file securely within your backend project folder later. You cannot download it again. - Click "Generate new application".
- You'll be redirected to the application details page. Note the Application ID.
1.4. Link Your Number to the Application:
- On the application details page, find the "Link virtual numbers" section.
- Click "Link" next to the Vonage number you purchased earlier.
You now have:
- API Key
- API Secret
- Application ID
private.key
file (downloaded)- Your Vonage phone number (sender number)
2. Setting Up the Backend (Node.js/Express)
Let's create the backend server that will handle scheduling requests and interact with Vonage.
2.1. Create Project Directory and Initialize:
# Create a main project folder
mkdir sms-scheduler-app
cd sms-scheduler-app
# Create the backend folder and navigate into it
mkdir backend
cd backend
# Initialize Node.js project
npm init -y
2.2. Install Dependencies:
npm install express @vonage/server-sdk node-cron dotenv cors
express
: Web framework.@vonage/server-sdk
: Official Vonage SDK for Node.js.node-cron
: Task scheduler.dotenv
: Loads environment variables from a.env
file.cors
: Enables Cross-Origin Resource Sharing.
2.3. Create .env
File:
Create a file named .env
in the backend
directory root. Add this file to your .gitignore
immediately to avoid committing secrets.
# .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 # Relative path to your key file
VONAGE_FROM_NUMBER=YOUR_VONAGE_PHONE_NUMBER # Number purchased from Vonage
# Server Configuration
PORT=5000
- Replace the
YOUR_
placeholders with your actual Vonage credentials and number. - Make sure
VONAGE_PRIVATE_KEY_PATH
points to the correct location where you will save theprivate.key
file.
2.4. Move private.key
:
Copy the private.key
file you downloaded earlier into the backend
directory.
2.5. Create Server File (server.js
):
Create a file named server.js
in the backend
directory.
// backend/server.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const cors = require('cors');
const cron = require('node-cron');
const { Vonage } = require('@vonage/server-sdk');
const { SMS } = require('@vonage/messages');
// --- Configuration ---
const app = express();
const port = process.env.PORT || 5000;
// --- Vonage Client Initialization ---
// Validate essential Vonage environment variables
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !process.env.VONAGE_FROM_NUMBER) {
console.error(""FATAL ERROR: Missing required Vonage environment variables. Check your .env file."");
process.exit(1); // Exit if configuration is incomplete
}
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH // Path to your private key file
});
// --- Middleware ---
app.use(cors()); // Enable CORS for all origins (adjust for production)
app.use(express.json()); // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// --- In-Memory Storage for Scheduled Jobs (Simple Example) ---
// WARNING: This is NOT production-ready. Jobs are lost on server restart.
// Use a persistent job queue (Redis/BullMQ, MongoDB/Agenda) for production.
const scheduledJobs = {};
// --- API Endpoints ---
// Basic Health Check
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// Schedule SMS Endpoint
app.post('/api/schedule', (req, res) => {
const { phoneNumber, message, dateTime } = req.body;
// 1. --- Input Validation ---
if (!phoneNumber || !message || !dateTime) {
return res.status(400).json({ error: 'Missing required fields: phoneNumber, message, dateTime' });
}
const targetDate = new Date(dateTime);
const now = new Date();
if (isNaN(targetDate.getTime())) {
return res.status(400).json({ error: 'Invalid dateTime format. Use ISO 8601 format (e.g., YYYY-MM-DDTHH:mm:ss).' });
}
if (targetDate <= now) {
return res.status(400).json({ error: 'Scheduled dateTime must be in the future.' });
}
// Basic phone number format check (E.164-like)
// NOTE: This regex is simple. For robust production use_ consider dedicated
// E.164 validation libraries (e.g._ google-libphonenumber) for better accuracy.
const phoneRegex = /^\+?[1-9]\d{1_14}$/;
if (!phoneRegex.test(phoneNumber)) {
return res.status(400).json({ error: 'Invalid phoneNumber format. Include country code (e.g._ +15551234567).' });
}
// 2. --- Convert Date to Cron Syntax ---
// node-cron uses format: 'Second Minute Hour DayOfMonth Month DayOfWeek'
const cronTime = `${targetDate.getSeconds()} ${targetDate.getMinutes()} ${targetDate.getHours()} ${targetDate.getDate()} ${targetDate.getMonth() + 1} *`;
// The '*' for DayOfWeek means it runs regardless of the day of the week_ matching the specific date.
// 3. --- Schedule the Job ---
try {
console.log(`[${new Date().toISOString()}] Scheduling SMS to ${phoneNumber} at ${targetDate.toISOString()} (Cron: ${cronTime})`);
const jobId = `${phoneNumber}-${targetDate.getTime()}-${Math.random().toString(36).substring(7)}`; // Unique ID
const task = cron.schedule(cronTime_ async () => {
console.log(`[${new Date().toISOString()}] Sending scheduled SMS to ${phoneNumber}`);
try {
const resp = await vonage.messages.send(
new SMS({
to: phoneNumber,
from: process.env.VONAGE_FROM_NUMBER, // Your Vonage sender number
text: message,
})
);
console.log(`[${new Date().toISOString()}] Message sent successfully to ${phoneNumber}. Message UUID: ${resp.messageUuid}`);
// Clean up the job reference after successful execution
if (scheduledJobs[jobId]) {
scheduledJobs[jobId].stop(); // Stop the cron task explicitly
delete scheduledJobs[jobId];
console.log(`[${new Date().toISOString()}] Cleaned up job ${jobId}`);
}
} catch (err) {
console.error(`[${new Date().toISOString()}] Error sending SMS to ${phoneNumber}:`, err.response ? err.response.data : err.message);
// Optionally: Implement retry logic here or notify an admin
if (scheduledJobs[jobId]) {
// Decide if the job should be stopped or retried on failure
scheduledJobs[jobId].stop();
delete scheduledJobs[jobId];
console.log(`[${new Date().toISOString()}] Cleaned up failed job ${jobId}`);
}
}
}, {
scheduled: true,
timezone: ""Etc/UTC"" // IMPORTANT: Specify timezone or ensure server/dates are UTC. Adjust if needed.
});
// Store the task reference (in-memory)
scheduledJobs[jobId] = task;
console.log(`[${new Date().toISOString()}] Job ${jobId} scheduled.`);
res.status(200).json({ success: true, message: `SMS scheduled successfully for ${targetDate.toISOString()}`, jobId: jobId });
} catch (error) {
console.error(`[${new Date().toISOString()}] Error scheduling job:`, error);
res.status(500).json({ error: 'Failed to schedule SMS.' });
}
});
// --- Optional: Webhook Endpoints (for status/inbound - basic logging) ---
app.post('/webhooks/status', (req, res) => {
console.log(`[${new Date().toISOString()}] Status Webhook Received:`, req.body);
// Process delivery receipts (DLRs) here if needed
res.status(200).end(); // Always respond 200 OK to Vonage webhooks
});
app.post('/webhooks/inbound', (req, res) => {
console.log(`[${new Date().toISOString()}] Inbound Webhook Received:`, req.body);
// Process incoming messages here if needed
res.status(200).end();
});
// --- Start Server ---
app.listen(port, () => {
console.log(`Backend server listening at http://localhost:${port}`);
console.warn(""--- IMPORTANT CAVEAT ---"");
console.warn(""This server uses an IN-MEMORY scheduler (`node-cron`). Scheduled jobs WILL BE LOST if the server restarts."");
console.warn(""For production, use a persistent job queue like BullMQ (with Redis) or Agenda (with MongoDB)."");
console.warn(""Ensure timezone consistency between frontend input, server processing, and `node-cron` timezone setting."");
console.warn(""-----------------------"");
});
Explanation:
- Imports & Config: Loads required modules and sets up Express.
- Vonage Client: Initializes the Vonage SDK using credentials from
.env
. Includes a check for missing variables. - Middleware: Enables CORS (adjust origin policy for production) and JSON body parsing.
- In-Memory Store (
scheduledJobs
): A simple object to hold references to thenode-cron
tasks. Crucially noted as not production-ready. /health
Endpoint: A simple check to see if the server is running./api/schedule
Endpoint (POST):- Extracts
phoneNumber
,message
,dateTime
from the request body. - Input Validation: Checks for missing fields, valid date format, future date, and basic phone number format (with a note about using more robust libraries for production).
- Date to Cron: Converts the JavaScript
Date
object into thenode-cron
specific time format. cron.schedule
:- Takes the cron time string and a callback function.
- The callback contains the logic to send the SMS using
vonage.messages.send
. - Includes basic logging for sending success/failure.
- Uses
async/await
for the Vonage API call. - Crucially, it stops and removes the job reference from
scheduledJobs
after execution (or failure) to prevent memory leaks in this simple setup. timezone: ""Etc/UTC""
is set. This is vital. Ensure thedateTime
sent from the frontend is also interpreted as UTC or convert appropriately. Mismatched timezones are a common source of scheduling errors.
- Stores the created
cron
task inscheduledJobs
using a unique ID. - Sends a success response to the frontend.
- Includes
try...catch
for scheduling errors.
- Extracts
- Webhook Endpoints: Basic handlers for Status and Inbound webhooks, just logging the received data for now. Responding with
200 OK
is essential. - Server Start: Starts the Express server and logs a warning about the in-memory scheduler.
2.6. Add Start Script to package.json
:
Open backend/package.json
and add a start
script:
// backend/package.json
{
// ... other fields
""scripts"": {
""start"": ""node server.js"", // Add this line
""test"": ""echo \""Error: no test specified\"" && exit 1""
},
// ... other fields
}
2.7. Run the Backend:
Open a terminal in the backend
directory:
npm start
You should see the server start message and the important caveat about the scheduler. Keep this terminal running.
3. Setting Up the Frontend (Vite/React)
Now, let's create the React user interface using Vite.
3.1. Create Vite Project:
Open a new terminal window. Navigate back to the main sms-scheduler-app
directory.
# Navigate back to the root project folder if you are in backend
cd ..
# Create the Vite/React frontend project
npm create vite@latest frontend -- --template react
# Navigate into the frontend folder
cd frontend
# Install dependencies
npm install
# Install axios for making API calls
npm install axios
3.2. Basic Styling (Optional):
You can add basic CSS or a UI library. For simplicity, let's add minimal styles to frontend/src/index.css
:
/* frontend/src/index.css (add or modify) */
body {
font-family: sans-serif;
padding: 20px;
line-height: 1.6;
}
#root {
max-width: 600px;
margin: 0 auto;
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
box-shadow: 2px 2px 10px rgba(0,0,0,0.05);
}
h1 {
text-align: center;
color: #333;
}
form {
display: flex;
flex-direction: column;
gap: 15px;
}
label {
font-weight: bold;
margin-bottom: -10px; /* Adjust spacing */
display: block; /* Ensure label takes block space */
}
input[type="text"],
input[type="tel"],
input[type="datetime-local"],
textarea {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
width: 100%; /* Make inputs fill container */
box-sizing: border-box; /* Include padding/border in width */
}
textarea {
min-height: 80px;
resize: vertical;
}
button {
padding: 12px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
margin-top: 10px; /* Add some space above button */
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.message {
padding: 10px;
margin-top: 15px;
border-radius: 4px;
text-align: center;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
small {
display: block; /* Ensure small text is on its own line */
margin-top: -10px; /* Adjust spacing below label */
margin-bottom: 5px; /* Add space below small text */
font-size: 0.8em;
color: #555;
}
3.3. Create the Scheduler Form Component:
Replace the contents of frontend/src/App.jsx
with the following:
// frontend/src/App.jsx
import React, { useState } from 'react';
import axios from 'axios';
import './index.css'; // Import the styles
// --- Configuration ---
// Use Vite's environment variable handling for the API URL
// Create a .env file in the 'frontend' directory with:
// VITE_API_URL=http://localhost:5000
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000'; // Fallback for safety
function App() {
const [phoneNumber, setPhoneNumber] = useState('');
const [message, setMessage] = useState('');
const [dateTime, setDateTime] = useState('');
const [statusMessage, setStatusMessage] = useState('');
const [messageType, setMessageType] = useState(''); // 'success' or 'error'
const [isLoading, setIsLoading] = useState(false);
// Function to get current datetime in YYYY-MM-DDTHH:mm format for min attribute
const getCurrentDateTimeLocal = () => {
const now = new Date();
// Adjust for local timezone offset
const offset = now.getTimezoneOffset();
const localDate = new Date(now.getTime() - (offset * 60 * 1000));
// Format to YYYY-MM-DDTHH:MM (required by datetime-local input)
return localDate.toISOString().slice(0, 16);
};
const handleSubmit = async (e) => {
e.preventDefault(); // Prevent default form submission
setIsLoading(true);
setStatusMessage('');
setMessageType('');
// --- Basic Frontend Validation ---
if (!phoneNumber || !message || !dateTime) {
setStatusMessage('Please fill in all fields.');
setMessageType('error');
setIsLoading(false);
return;
}
// Ensure datetime is in the future (basic check, backend does more thorough validation)
const selectedDate = new Date(dateTime);
if (selectedDate <= new Date()) {
setStatusMessage('Scheduled date and time must be in the future.');
setMessageType('error');
setIsLoading(false);
return;
}
// --- Prepare Data for API ---
// Ensure dateTime sent to backend is in a consistent format (ISO 8601 UTC is recommended)
// The input type="datetime-local" provides local time. Convert to UTC before sending.
const scheduleData = {
phoneNumber_
message_
dateTime: selectedDate.toISOString()_ // Send as ISO 8601 UTC string
};
try {
// --- Make API Call ---
const response = await axios.post(`${API_BASE_URL}/api/schedule`_ scheduleData);
if (response.status === 200 && response.data.success) {
setStatusMessage(`Success! ${response.data.message}`);
setMessageType('success');
// Optionally clear the form
// setPhoneNumber('');
// setMessage('');
// setDateTime('');
} else {
// Handle cases where backend might return 200 but with an error payload
setStatusMessage(response.data.error || 'An unexpected success response occurred.');
setMessageType('error');
}
} catch (error) {
console.error('Error submitting schedule:'_ error);
let errorMessage = 'Failed to schedule SMS. Please try again.';
if (error.response && error.response.data && error.response.data.error) {
// Use specific error from backend if available
errorMessage = error.response.data.error;
} else if (error.request) {
// Network error (backend unreachable)
errorMessage = `Network error. Could not reach the server at ${API_BASE_URL}. Is it running?`;
}
setStatusMessage(errorMessage);
setMessageType('error');
} finally {
setIsLoading(false); // Re-enable button
}
};
return (
<div>
<h1>SMS Scheduler</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="phoneNumber">Recipient Phone Number:</label>
<input
type="tel"
id="phoneNumber" // ID matches htmlFor
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="+15551234567" // Example format
required
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="message">Message:</label>
<textarea
id="message" // ID matches htmlFor
value={message}
onChange={(e) => setMessage(e.target.value)}
maxLength={160} // Standard SMS length limit (consider segments for longer)
required
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="dateTime">Schedule Date & Time:</label>
<small>Select the date and time in your local timezone.</small>
<input
type="datetime-local"
id="dateTime" // ID matches htmlFor
value={dateTime}
onChange={(e) => setDateTime(e.target.value)}
min={getCurrentDateTimeLocal()} // Prevent selecting past dates/times
required
disabled={isLoading}
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Scheduling...' : 'Schedule SMS'}
</button>
</form>
{statusMessage && (
<div className={`message ${messageType}`}>
{statusMessage}
</div>
)}
</div>
);
}
export default App;
Explanation:
- State: Uses
useState
to manage form inputs (phoneNumber
,message
,dateTime
), loading state (isLoading
), and status messages (statusMessage
,messageType
). - API URL: Reads the backend URL from Vite's environment variables (
import.meta.env.VITE_API_URL
). You'll need to create a.env
file in thefrontend
directory containingVITE_API_URL=http://localhost:5000
for local development. getCurrentDateTimeLocal
: Helper function to set themin
attribute on the date/time input, preventing users from selecting past times in their local timezone.handleSubmit
:- Prevents default form submission.
- Sets loading state and clears previous status messages.
- Performs basic frontend validation (presence check, future date).
- Constructs the
scheduleData
object. Crucially, it converts the localdateTime
input value to an ISO 8601 UTC string usingnew Date(dateTime).toISOString()
before sending. This ensures consistency when the backend (set to UTC) processes it. - Uses
axios.post
to send the data to the backend's/api/schedule
endpoint using theAPI_BASE_URL
. - Handles success: displays the success message from the backend.
- Handles errors: Catches Axios errors (network issues, backend errors) and displays an appropriate error message, prioritizing the specific error sent back from the backend API if available. Provides a more helpful network error message.
- Uses
finally
to reset the loading state.
- JSX Form:
- Renders standard HTML form elements.
- Uses controlled components (input values tied to state).
- Correctly links labels to inputs using matching
htmlFor
andid
attributes. - Includes
placeholder
for the phone number format. - Sets
maxLength
for the message. - Uses
type="datetime-local"
for easy date/time selection. Sets themin
attribute using our helper function. Includes helper text. - Disables inputs and the button while
isLoading
is true. - Conditionally renders a status message (
.message.success
or.message.error
) based on the API response.
3.4. Create Frontend .env
file:
In the frontend
directory, create a file named .env
:
# frontend/.env
VITE_API_URL=http://localhost:5000
(Remember to add frontend/.env
to your main .gitignore
file)
3.5. Run the Frontend:
Ensure your backend server is still running (from step 2.7). Open a terminal in the frontend
directory:
npm run dev
Vite will start the development server, usually at http://localhost:5173
(check your terminal output). Open this URL in your browser.
4. Verification and Testing
Now, let's test the end-to-end flow.
- Open the Frontend: Navigate to the URL provided by Vite (e.g.,
http://localhost:5173
). - Fill the Form:
- Recipient Phone Number: Enter your own mobile number in E.164 format (e.g.,
+15551234567
). - Message: Type a test message.
- Schedule Date & Time: Select a date and time a few minutes into the future using the date picker.
- Recipient Phone Number: Enter your own mobile number in E.164 format (e.g.,
- Submit: Click ""Schedule SMS"".
- Check Frontend: You should see a ""Success! SMS scheduled successfully..."" message.
- Check Backend Logs: Look at the terminal where your backend (
npm start
) is running. You should see logs like:[timestamp] Scheduling SMS to +15551234567 at [ISO timestamp] (Cron: [cron syntax])
[timestamp] Job [jobId] scheduled.
- Wait: Wait until the scheduled time arrives.
- Check Backend Logs Again: At the scheduled time, you should see:
[timestamp] Sending scheduled SMS to +15551234567
[timestamp] Message sent successfully to +15551234567. Message UUID: [UUID]
[timestamp] Cleaned up job [jobId]
- Check Your Phone: You should receive the SMS message you scheduled!
- (Optional) Test Errors:
- Try submitting without filling all fields.
- Try submitting with a past date/time.
- Try submitting with an invalid phone number format (e.g.,
12345
,+1-555-1234
). - Observe the error messages displayed on the frontend. Check backend logs for corresponding 400 Bad Request errors.
- Stop the backend server and try submitting to test the network error handling on the frontend.
5. Troubleshooting and Caveats
- CRITICAL: In-Memory Scheduling is NOT Production-Ready: The biggest caveat is using
node-cron
with an in-memory store (scheduledJobs
). If your Node.js server restarts for any reason (crash, deployment, scaling event), all scheduled jobs that haven't run yet will be lost.- Production Solution: Use a persistent job queue system. Popular choices include:
- BullMQ (or Bull): Requires Redis. Robust, feature-rich, widely used.
- Agenda: Requires MongoDB. Good option if you already use MongoDB.
- These systems store job details externally, allowing your application to pick them up even after restarts.
- Production Solution: Use a persistent job queue system. Popular choices include:
- Time Zones: Handling dates and times across different user timezones and server timezones is complex.
- Our Approach: The frontend captures local time, converts it to ISO 8601 (UTC) for the API request (
new Date(localDateTime).toISOString()
). The backend usesnode-cron
configured withtimezone: "Etc/UTC"
. This works if the conversion is correct and the server reliably processes UTC. - Robustness: Consider libraries like
date-fns-tz
ormoment-timezone
for explicit timezone handling and conversion on both frontend and backend if precise local time scheduling is critical. Always store and process dates in UTC on the backend whenever possible.
- Our Approach: The frontend captures local time, converts it to ISO 8601 (UTC) for the API request (
- Vonage Errors: Check backend logs for errors from the Vonage SDK. Common issues:
Authentication failure
: Incorrect API Key/Secret/Application ID/Private Key. Verify.env
and key file path.Invalid parameters
: Check phone number format (E.164 recommended:+15551234567
), message content.Insufficient funds
: Add credit to your Vonage account.Illegal Sender Address
: EnsureVONAGE_FROM_NUMBER
is a valid Vonage number linked to your application and capable of sending SMS to the destination.
- CORS Errors: If the frontend cannot reach the backend, check the browser's developer console for CORS errors. Ensure
app.use(cors());
is present inserver.js
. For production, configure CORS more strictly (e.g.,app.use(cors({ origin: 'YOUR_DEPLOYED_FRONTEND_URL' }));
). - Phone Number Formatting: Use the E.164 standard (
+
followed by country code and number) for best results globally. The simple regex in the backend validation might need refinement; consider dedicated libraries (likegoogle-libphonenumber
) for production. - Rate Limiting: Implement rate limiting on the
/api/schedule
endpoint (e.g., usingexpress-rate-limit
) to prevent abuse. - Error Handling/Retries: The current error handling is basic. Production systems should have more robust error logging, potentially implement retry mechanisms for transient Vonage API errors, and alert administrators to persistent failures.