code examples
code examples
Build SMS Scheduling and Reminders with Vite, React, Node.js, and Vonage Messages API
Complete step-by-step guide to building an SMS scheduling application with Vite, React, Node.js, Express, and Vonage Messages API. Learn to schedule SMS reminders, handle webhooks, manage timezones, and deploy production-ready solutions with BullMQ or Agenda.
Build SMS Scheduling and Reminders with Vite, React, Node.js, and Vonage Messages API
Learn how to schedule SMS messages in Node.js using the Vonage Messages API, React, and Vite. This comprehensive tutorial walks you through building a production-ready SMS scheduler with proper job queue management, webhook handling, and timezone support.
Build a functional web application where users input a phone number, message, and future date/time to schedule SMS sending via Vonage. This hands-on guide covers everything from initial setup to production deployment, including environment management, asynchronous job handling with node-cron, error logging, and basic security. Important: While this tutorial uses node-cron with in-memory storage for learning purposes, production applications require persistent job queues like BullMQ or Agenda, as explained in the production considerations section.
What You'll Learn: SMS Scheduling with Node.js and Vonage
What You'll Build:
- A React frontend (built with Vite) where users submit phone numbers, message content, and specific date/time for SMS sending.
- A Node.js (Express) backend API that receives scheduling requests from the frontend.
- Integration with the Vonage Messages API to send scheduled SMS messages.
- A scheduling mechanism (
node-cron) within the backend to trigger SMS sending at specified times.
Use Cases for SMS Scheduling:
Schedule automated SMS notifications and reminders through a simple interface – ideal for:
- Appointment confirmations and reminders for healthcare, salons, and service businesses
- Event notifications and calendar reminders
- Time-sensitive alerts and follow-up messages
- Marketing campaigns with scheduled delivery
- Two-factor authentication (2FA) with delayed verification codes
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): Node.js version 20.19+ or 22.12+ installed on your system (required for Vite 7 as of 2025). Node.js 18 support has been dropped as it reached End-of-Life in April 2025. 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.
Step 1: Set Up Your Vonage Account and Messages API 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
/apilike the scheduling endpoint.)
- Inbound URL: Enter
- Generate Public and Private Key: Click this button. A
private.keyfile 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.keyfile (downloaded)- Your Vonage phone number (sender number)
Step 2: Build the Node.js Backend with Express and Vonage SDK
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 -y2.2. Install Dependencies:
npm install express @vonage/server-sdk node-cron dotenv corsexpress: Web framework.@vonage/server-sdk: Official Vonage SDK for Node.js.node-cron: Task scheduler.dotenv: Loads environment variables from a.envfile.cors: Enables Cross-Origin Resource Sharing.
Note: This guide uses Vonage Node.js SDK v3.25.1 (latest as of September 2025). The Messages API has General Availability status. The SDK uses a promise-based approach for all API calls, enabling clean async/await syntax. Source: Vonage Node.js SDK.
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_PATHpoints to the correct location where you will save theprivate.keyfile.
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.
// E.164 Standard (ITU-T): Maximum 15 digits total, starting with '+' followed by
// 1-3 digit country code and up to 12 digit subscriber number. No spaces or separators.
// Source: ITU-T Recommendation E.164 (International public telecommunication numbering plan).
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-crontasks. Crucially noted as not production-ready. /healthEndpoint: A simple check to see if the server is running./api/scheduleEndpoint (POST):- Extracts
phoneNumber,message,dateTimefrom 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
Dateobject into thenode-cronspecific 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/awaitfor the Vonage API call. - Crucially, it stops and removes the job reference from
scheduledJobsafter execution (or failure) to prevent memory leaks in this simple setup. timezone: "Etc/UTC"is set. This is vital. Ensure thedateTimesent from the frontend is also interpreted as UTC or convert appropriately. Mismatched timezones are a common source of scheduling errors.
- Stores the created
crontask inscheduledJobsusing a unique ID. - Sends a success response to the frontend.
- Includes
try...catchfor scheduling errors.
- Extracts
- Webhook Endpoints: Basic handlers for Status and Inbound webhooks, just logging the received data for now. Responding with
200 OKis 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 startYou should see the server start message and the important caveat about the scheduler. Keep this terminal running.
Step 3: Create the React Frontend with Vite for SMS Scheduling
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 axios3.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
useStateto 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.envfile in thefrontenddirectory containingVITE_API_URL=http://localhost:5000for local development. getCurrentDateTimeLocal: Helper function to set theminattribute 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
scheduleDataobject. Crucially, it converts the localdateTimeinput 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.postto send the data to the backend's/api/scheduleendpoint 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
finallyto 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
htmlForandidattributes. - Includes
placeholderfor the phone number format. - Sets
maxLengthfor the message. - Uses
type="datetime-local"for easy date/time selection. Sets theminattribute using our helper function. Includes helper text. - Disables inputs and the button while
isLoadingis true. - Conditionally renders a status message (
.message.successor.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 devVite will start the development server, usually at http://localhost:5173 (check your terminal output). Open this URL in your browser.
Step 4: Test and Verify Your SMS Scheduler End-to-End
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.
Step 5: Troubleshooting, Production Deployment, and Best Practices
- CRITICAL: In-Memory Scheduling is NOT Production-Ready: The biggest caveat is using
node-cronwith 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. Node-cron executes jobs only as long as your script runs and lacks persistence once the script exits, making it suitable only for learning and development, not production use.- Production Solution - Persistent Job Queues: For production SMS scheduling, use a persistent job queue system that survives server restarts:
- BullMQ (Recommended): Redis-based job queue with superior performance, lower latency, and higher throughput. BullMQ stores all jobs in Redis, ensuring persistence across restarts. Key features include concurrent job processing, automatic retry logic for failed SMS sends, horizontal scaling across multiple servers, job prioritization, and built-in monitoring. BullMQ is a modern TypeScript rewrite of Bull with an improved API and better developer experience. Perfect for high-volume SMS campaigns. Source: BullMQ Documentation.
- Agenda: MongoDB-based job scheduler optimized for applications already using MongoDB. Includes a lock mechanism to prevent duplicate job execution when multiple Agenda instances run concurrently. Best suited for projects where MongoDB is the primary database. Agenda operates within a single process by default with less horizontal scalability than BullMQ. Source: Agenda GitHub.
- When to Choose Which: Use BullMQ for high-performance, distributed SMS scheduling with Redis. Choose Agenda if you already use MongoDB and prefer a simpler setup without additional infrastructure.
- Production Solution - Persistent Job Queues: For production SMS scheduling, use a persistent job queue system that survives server restarts:
- 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-cronconfigured withtimezone: "Etc/UTC". This works if the conversion is correct and the server reliably processes UTC. - Robustness: Consider libraries like
date-fns-tzormoment-timezonefor 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.envand 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_NUMBERis 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/scheduleendpoint (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.
Frequently Asked Questions
How do I schedule SMS messages with Node.js and Vonage Messages API?
To schedule SMS with Node.js and Vonage, create an Express backend using the Vonage Node.js SDK (v3.25.1+) and a job scheduler. For development, use node-cron to convert datetime to cron syntax and trigger vonage.messages.send() at the specified time. For production, implement BullMQ with Redis or Agenda with MongoDB to ensure scheduled messages persist across server restarts. Users submit phone numbers, messages, and scheduled times through a React frontend built with Vite.
What Node.js version does Vite 7 require?
Vite 7 requires Node.js version 20.19+ or 22.12+. Node.js 18 support was dropped as it reached End-of-Life in April 2025. Ensure you install a compatible Node.js version before starting your Vite React project.
Why is node-cron not suitable for production SMS scheduling?
Node-cron stores scheduled jobs only in memory, meaning all pending jobs are lost if your Node.js server restarts for any reason (crashes, deployments, scaling). Production applications require persistent job queues like BullMQ (Redis-backed) or Agenda (MongoDB-backed) that survive server restarts.
What is the difference between BullMQ and Agenda for job scheduling?
BullMQ uses Redis for persistence and excels at high-performance, distributed job processing with lower latency, concurrent execution, automatic retries, and horizontal scaling. Agenda uses MongoDB and integrates well with MongoDB-based applications but operates in a single process by default with less horizontal scalability. Choose BullMQ for performance-critical applications and Agenda if you already use MongoDB.
What is the E.164 phone number format?
E.164 is the ITU-T international standard for phone numbers. It allows a maximum of 15 digits total: a '+' prefix, followed by a 1-3 digit country code and up to 12 digit subscriber number. No spaces or separators are allowed. Examples: +12125551234 (US), +442012345678 (UK).
How do I handle timezone issues when scheduling SMS?
Capture local time in the frontend, convert it to ISO 8601 UTC format using new Date(localDateTime).toISOString() before sending to the backend. Configure your scheduler (node-cron) with timezone: "Etc/UTC" and always process dates in UTC on the backend. For precise timezone handling, use libraries like date-fns-tz or moment-timezone.
What Vonage credentials do I need for SMS scheduling?
You need four Vonage credentials: API Key and API Secret (from dashboard homepage), Application ID (from your Messages API application), and a Private Key file (downloaded when creating the application). You also need a Vonage virtual phone number with SMS capability linked to your application.
How do I test SMS scheduling locally with Vite and Node.js?
Run your Node.js backend on port 5000 (npm start in backend directory) and Vite dev server on port 5173 (npm run dev in frontend directory). Enter your own phone number in E.164 format, create a message, select a future time a few minutes away, and submit. Check backend logs for scheduling confirmation and wait for the SMS to arrive.
What causes CORS errors between Vite frontend and Express backend?
CORS errors occur when the frontend (running on http://localhost:5173) cannot access the backend (on http://localhost:5000) due to same-origin policy. Fix by adding app.use(cors()); middleware in your Express server.js file. For production, restrict origins with app.use(cors({ origin: 'YOUR_DEPLOYED_FRONTEND_URL' }));.
How do I validate phone numbers for SMS in Node.js?
Use the E.164 standard format validation. For basic validation, use a regex like /^\+?[1-9]\d{1,14}$/ that checks for 1-15 digits starting with a non-zero digit. For production-grade validation across all countries and regions, use dedicated libraries like google-libphonenumber that handle complex country-specific rules.