Developer guide: Building a Node.js SMS scheduling and reminder service with Vonage
This guide provides a step-by-step walkthrough for creating a robust SMS reminder service using Node.js, Express, and the Vonage Messages API. You'll learn how to schedule SMS messages to be sent at specific future times, manage these reminders via an API, and integrate securely with Vonage.
This service solves the common need to notify users via SMS about appointments, deadlines, or other time-sensitive events without requiring manual intervention at the time of sending.
Technologies used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express: A minimal and flexible Node.js web application framework for building the API.
- Vonage Messages API: Enables sending SMS (and other message types) programmatically.
node-cron
: A simple cron-like job scheduler for Node.js (potentially useful for periodic tasks like cleanup, though this guide primarily usessetTimeout
for direct scheduling).dotenv
: Loads environment variables from a.env
file.uuid
: Generates unique identifiers for reminders.- (Optional but Recommended) SQLite: A simple file-based database for persistent storage of reminders.
System architecture:
+---------+ +-----------------+ +-----------------+ +--------+ +------+
| User | ----> | Express API | ----> | Reminder Logic | ----> | Vonage | ----> | SMS |
| (Admin) | | (POST /schedule)| | (setTimeout/DB) | | API | | User |
+---------+ +-----------------+ +-----------------+ +--------+ +------+
| ^ |
| GET /reminders/:id | |
| DELETE /reminders/:id| |
+---------------------+----------------------------------------------+
Final outcome:
By the end of this guide, you will have a Node.js application with API endpoints to:
- Schedule an SMS reminder for a specific recipient, message, and future time.
- (Optional) List scheduled reminders.
- (Optional) Cancel a pending reminder.
- Securely store credentials and handle errors gracefully.
- (Optional) Persist reminder data in a database.
Prerequisites:
- Node.js and npm (or yarn): Installed on your system. Download Node.js
- Vonage API Account: Sign up for free at Vonage.
- Vonage API Key and Secret: Found on your Vonage API Dashboard.
- Vonage Application: You'll create one to get an Application ID and Private Key.
- Vonage Virtual Number: Purchase one from the Vonage Dashboard (Numbers > Buy Numbers) capable of sending SMS. Link this number to your Vonage Application.
- (Optional for Local Development)
ngrok
: Useful for exposing your local server to the internet, primarily for receiving webhook callbacks from Vonage (like delivery receipts or inbound messages), which are not covered in the basic implementation of this guide but mentioned as potential extensions. It's not required just for scheduling and sending outbound SMS. Get ngrok
1. Setting up the project
Let's initialize the Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir vonage-sms-scheduler cd vonage-sms-scheduler
-
Initialize npm Project: This creates a
package.json
file.npm init -y
-
Install Dependencies: We need Express for the server, the Vonage SDK,
dotenv
for environment variables,uuid
for unique IDs, andnode-cron
(though optional for this guide's core logic).npm install express @vonage/server-sdk dotenv node-cron uuid
(Optional: For database persistence)
npm install sqlite3
(Optional: For robust input validation)
npm install express-validator
-
Create Project Structure: Create the basic files and directories.
touch server.js .env .gitignore mkdir data # Optional: For SQLite database file mkdir src touch src/vonageClient.js src/scheduler.js mkdir src/routes touch src/routes/reminderRoutes.js # Optional: For DB touch src/database.js
Your structure should look something like this:
vonage-sms-scheduler/ ├── data/ ├── node_modules/ ├── src/ │ ├── database.js # Optional DB logic │ ├── scheduler.js # Scheduling logic │ ├── vonageClient.js # Vonage SDK setup │ └── routes/ │ └── reminderRoutes.js # API routes ├── .env # Environment variables (API keys, etc.) ├── .gitignore # Files/folders to ignore in Git ├── package.json ├── package-lock.json └── server.js # Main Express server setup
-
Configure
.gitignore
: Prevent sensitive information and unnecessary files from being committed to version control. Add the following to your.gitignore
file:# Dependencies node_modules/ # Environment variables .env # Optional database file data/*.sqlite # Logs *.log private.key # Example if storing key in root # OS generated files .DS_Store Thumbs.db
-
Set Up Vonage Application and Credentials:
- Go to your Vonage API Dashboard.
- Click "Create a new application".
- Give it a name (e.g., "Node SMS Scheduler").
- Click "Generate public and private key". Save the
private.key
file that downloads. For simplicity in this guide, you can save it directly into your project's root directory (vonage-sms-scheduler/private.key
), ensuring.gitignore
includesprivate.key
or*.key
. Security Warning: Storing private keys directly in the project directory, even if gitignored, is generally discouraged for production environments. Consider storing it outside the project root, using environment variables to pass the key content (not the path), or leveraging secrets management services. - Enable the "Messages" capability. For sending scheduled messages, you don't strictly need to configure Inbound/Status URLs immediately, but if you plan to handle replies or delivery receipts later, set them up using an
ngrok
URL (e.g.,https://<your-ngrok-url>.ngrok.io/webhooks/inbound
and/status
). - Scroll down to "Link virtual numbers" and link the Vonage number you purchased earlier to this application.
- Click "Create application".
- Note down the Application ID.
-
Configure Environment Variables (
.env
): Create a.env
file in the project root and add your Vonage credentials and configuration. Never commit this file to Git.# .env # Vonage Credentials # Found in Vonage Dashboard > API Settings VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET # Vonage Application Credentials # From the application you created VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID # Path relative to the project root where you saved the private key VONAGE_PRIVATE_KEY_PATH=./private.key # Vonage Number associated with your application VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # e.g., 14155550100 # Server Configuration PORT=3000 # Optional: Database path DATABASE_PATH=./data/reminders.sqlite # Optional: Simple API Key for endpoint protection # INTERNAL_API_KEY=your-secret-api-key
VONAGE_API_KEY
,VONAGE_API_SECRET
: Get from the main Vonage Dashboard API settings page.VONAGE_APPLICATION_ID
: Get from the specific Vonage Application page you created.VONAGE_PRIVATE_KEY_PATH
: The path to theprivate.key
file you downloaded when creating the application.VONAGE_NUMBER
: The Vonage virtual number you linked to the application, in E.164 format (e.g., 12015550123).PORT
: The port your Express server will listen on.DATABASE_PATH
: Location for the SQLite database file (if using).INTERNAL_API_KEY
: (Optional) A secret key you can define to protect your API endpoints.
2. Integrating with Vonage (SDK Setup)
Let's configure the Vonage Node.js SDK to interact with the API using the credentials from our .env
file.
File: src/vonageClient.js
// src/vonageClient.js
const { Vonage } = require('@vonage/server-sdk');
const { Auth } = require('@vonage/auth');
const fs = require('fs'); // Needed to check private key path
require('dotenv').config(); // Load environment variables
// --- Input Validation ---
// Basic check to ensure required environment variables are set
const requiredEnvVars = [
'VONAGE_API_KEY',
'VONAGE_API_SECRET',
'VONAGE_APPLICATION_ID',
'VONAGE_PRIVATE_KEY_PATH',
'VONAGE_NUMBER'
];
let hasError = false;
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
console.error(`Error: Missing required environment variable ${envVar}. Please check your .env file.`);
hasError = true;
}
}
// Check if private key file exists
if (process.env.VONAGE_PRIVATE_KEY_PATH && !fs.existsSync(process.env.VONAGE_PRIVATE_KEY_PATH)) {
console.error(`Error: Private key file not found at path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`);
hasError = true;
}
if (hasError) {
process.exit(1); // Exit if critical config is missing or invalid
}
// --- Vonage Authentication ---
// Use Application ID and Private Key for authentication with Messages API
// The Vonage SDK requires API Key/Secret for basic instantiation, even if subsequent calls
// like Messages API primarily use Application ID/Private Key for JWT generation.
const credentials = new Auth({
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,
});
// --- Vonage Client Initialization ---
const vonage = new Vonage(credentials);
// --- SMS Sending Function ---
/**
* Sends an SMS message using the Vonage Messages API.
* @param {string} to - The recipient phone number (E.164 format).
* @param {string} text - The message content.
* @returns {Promise<string>} - Resolves with the message UUID on success.
* @throws {Error} - Rejects with an error if sending fails.
*/
async function sendSms(to, text) {
const fromNumber = process.env.VONAGE_NUMBER;
console.log(`Attempting to send SMS from ${fromNumber} to ${to}`);
try {
const resp = await vonage.messages.send({
message_type: ""text"",
text: text,
to: to,
from: fromNumber,
channel: ""sms""
});
console.log(`SMS sent successfully to ${to}. Message UUID: ${resp.message_uuid}`);
return resp.message_uuid;
} catch (err) {
console.error(`Error sending SMS to ${to}:`, err.response ? err.response.data : err.message);
// Rethrow a more specific error or handle as needed
throw new Error(`Failed to send SMS via Vonage: ${err.message}`);
}
}
module.exports = {
sendSms,
vonage // Exporting vonage instance might be useful for other API calls
};
Explanation:
- Load Env Vars:
dotenv.config()
loads variables from.env
. - Validation: Includes checks for crucial Vonage environment variables and verifies the existence of the private key file.
- Authentication: We use the
Auth
object withapplicationId
andprivateKey
for the Messages API, as recommended. The SDK also requiresapiKey
andapiSecret
for initialization. - Client Initialization: Creates the
vonage
instance. sendSms
Function:- Takes the recipient
to
number andtext
message. - Retrieves the
VONAGE_NUMBER
from environment variables. - Uses
vonage.messages.send()
specifyingchannel: ""sms""
,message_type: ""text""
. - Includes logging for success and detailed error logging, accessing
err.response.data
if available. - Returns the
message_uuid
on success or throws an error on failure.
- Takes the recipient
3. Implementing Core Functionality (Scheduling Logic)
We'll use Node.js's built-in setTimeout
function to schedule the SMS sending for a specific future time. We'll manage pending timers in memory initially. Note: This approach is simple but not robust against server restarts; scheduled reminders will be lost. See Section 5 for database persistence.
File: src/scheduler.js
// src/scheduler.js
const { sendSms } = require('./vonageClient');
const { v4: uuidv4 } = require('uuid'); // Use uuid for unique IDs
// --- In-Memory Storage for Pending Reminders ---
// WARNING: This is NOT persistent. If the server restarts, pending reminders are lost.
// A database (see Section 5) is strongly recommended for reliability.
const pendingReminders = new Map(); // Map<reminderId_ { timeoutId: NodeJS.Timeout_ recipient: string_ message: string_ sendAt: Date_ status: string }>
/**
* Schedules an SMS reminder using in-memory storage.
* @param {string} recipient - Recipient phone number (E.164 format).
* @param {string} message - Message content.
* @param {Date} sendAtDate - The Date object representing when to send the message.
* @returns {string} The unique ID of the scheduled reminder.
* @throws {Error} If scheduling fails (e.g., time is in the past).
*/
function scheduleReminder(recipient, message, sendAtDate) {
const now = new Date();
const delay = sendAtDate.getTime() - now.getTime();
if (delay <= 0) {
throw new Error("Scheduled time must be in the future.");
}
// Limit delay to avoid issues with extremely long timeouts (optional)
// const maxDelay = 2147483647; // Max 32-bit integer (~24.8 days)
// if (delay > maxDelay) {
// throw new Error(`Scheduling delay too long (max ~24.8 days). Consider using a persistent job queue for longer schedules.`);
// }
const reminderId = uuidv4(); // Generate a unique ID
console.log(`Scheduling reminder ${reminderId} for ${recipient} at ${sendAtDate.toISOString()} (in ${delay}ms)`);
const timeoutId = setTimeout(async () => {
console.log(`Executing reminder ${reminderId} for ${recipient}`);
const reminder = pendingReminders.get(reminderId); // Get latest state
if (!reminder || reminder.status !== 'pending') {
console.warn(`Reminder ${reminderId} execution skipped: No longer pending (status: ${reminder?.status})`);
pendingReminders.delete(reminderId); // Clean up if somehow still present
return;
}
try {
const messageUuid = await sendSms(recipient, message);
console.log(`Reminder ${reminderId} sent successfully. Vonage Message ID: ${messageUuid}`);
// Update status in memory
pendingReminders.set(reminderId, { ...reminder, status: 'sent', vonageMessageId: messageUuid });
// Optionally update status in DB here if using persistence
} catch (error) {
console.error(`Failed to send reminder ${reminderId}:`, error);
// Update status in memory
pendingReminders.set(reminderId, { ...reminder, status: 'failed' });
// Optionally update status in DB here (e.g., 'failed')
} finally {
// Remove from active map *after* processing (sent or failed) for status query purposes
// Or keep it with final status, depending on requirements for getReminderStatus
// For this simple version, let's keep the final status available for a while.
// A cleanup mechanism might be needed for long-running servers.
console.log(`Reminder ${reminderId} processed. Final status: ${pendingReminders.get(reminderId)?.status}`);
}
}, delay);
// Store reminder details and the timeout handle
pendingReminders.set(reminderId, {
timeoutId,
recipient,
message,
sendAt: sendAtDate,
status: 'pending' // Initial status
});
return reminderId;
}
/**
* Cancels a pending reminder (in-memory version).
* @param {string} reminderId - The ID of the reminder to cancel.
* @returns {boolean} True if cancelled successfully, false otherwise (e.g., not found or already processed).
*/
function cancelReminder(reminderId) {
const reminder = pendingReminders.get(reminderId);
if (reminder && reminder.status === 'pending') {
clearTimeout(reminder.timeoutId); // Clear the scheduled timeout
// Update status in memory
pendingReminders.set(reminderId, { ...reminder, status: 'cancelled' });
console.log(`Cancelled reminder ${reminderId}`);
// Optionally update status in DB here
return true;
}
console.log(`Reminder ${reminderId} not found or cannot be cancelled (status: ${reminder?.status}).`);
return false;
}
/**
* Gets the details of a reminder (pending, sent, failed, cancelled) from memory.
* @param {string} reminderId - The ID of the reminder.
* @returns {object | undefined} Reminder details (excluding timeoutId) or undefined if not found.
*/
function getReminderStatus(reminderId) {
const reminder = pendingReminders.get(reminderId);
if(reminder) {
// Return a copy without the internal timeoutId
const { timeoutId, ...details } = reminder;
return details;
}
return undefined;
}
module.exports = {
scheduleReminder,
cancelReminder,
getReminderStatus,
};
Explanation:
pendingReminders
Map: An in-memoryMap
stores reminder states. The key isreminderId
, value includestimeoutId
, details, andstatus
. Data is volatile.scheduleReminder
:- Calculates
delay
. Checks if it's in the future. - Generates
reminderId
usinguuid
. - Uses
setTimeout
. The callback checks the reminder status before sending (in case it was cancelled), callssendSms
, and updates the in-memory status to 'sent' or 'failed'. - Stores initial details and
timeoutId
inpendingReminders
with 'pending' status. - Returns
reminderId
.
- Calculates
cancelReminder
:- Looks up the reminder. Checks if its status is 'pending'.
- If yes, calls
clearTimeout
and updates the in-memory status to 'cancelled'. - Returns
true
if cancelled,false
otherwise.
getReminderStatus
:- Retrieves reminder details from the map (excluding
timeoutId
). Returns the current state ('pending', 'sent', 'failed', 'cancelled') if found.
- Retrieves reminder details from the map (excluding
4. Building the API Layer (Express Routes)
Now, let's create the Express server and define API endpoints to interact with our scheduler.
File: src/routes/reminderRoutes.js
// src/routes/reminderRoutes.js
const express = require('express');
const { scheduleReminder, cancelReminder, getReminderStatus } = require('../scheduler');
// Optional: Input validation library
// const { body, param, validationResult } = require('express-validator');
const router = express.Router();
// --- Middleware for Basic Input Validation (Example) ---
// For production applications, using a dedicated validation library like express-validator
// is highly recommended for more robust and maintainable validation.
// This example shows basic manual validation.
const validateScheduleRequest = (req, res, next) => {
const { recipient, message, sendAt } = req.body;
if (!recipient || !message || !sendAt) {
return res.status(400).json({ error: 'Missing required fields: recipient, message, sendAt' });
}
// Basic phone number format check (E.164 recommended)
if (!/^\+?[1-9]\d{1,14}$/.test(recipient)) {
return res.status(400).json({ error: 'Invalid recipient phone number format (should be E.164, e.g., +14155550100)' });
}
// Check if sendAt is a valid date string parseable by new Date()
const sendAtDate = new Date(sendAt);
if (isNaN(sendAtDate.getTime())) {
// More specific check for ISO 8601 format might be desired
return res.status(400).json({ error: 'Invalid sendAt format. Use a valid date string (ISO 8601 recommended, e.g., 2025-12-31T23:59:59.000Z)' });
}
// Attach parsed date to request object for handler use
req.sendAtDate = sendAtDate;
next();
};
// --- API Endpoints ---
// POST /api/reminders - Schedule a new reminder
router.post('/', validateScheduleRequest, (req, res, next) => {
const { recipient, message } = req.body;
const { sendAtDate } = req; // Get parsed date from validation middleware
try {
const reminderId = scheduleReminder(recipient, message, sendAtDate);
res.status(202).json({
message: ""Reminder scheduled successfully."",
reminderId: reminderId,
details: {
recipient: recipient,
message: message,
sendAt: sendAtDate.toISOString() // Return consistent ISO string format
}
});
} catch (error) {
// Handle specific scheduling errors like ""time in the past""
if (error.message.includes(""future"")) {
return res.status(400).json({ error: error.message });
}
// Pass other errors (e.g., from uuid, setTimeout setup) to the central error handler
next(error);
}
});
// GET /api/reminders/:id - Get status of a specific reminder
router.get('/:id', async (req, res, next) => { // Mark as async if DB check is added
const { id } = req.params;
try {
const reminder = getReminderStatus(id); // Check in-memory store first
if (reminder) {
res.status(200).json(reminder);
} else {
// Optional: If using DB persistence, check the database here
// const dbReminder = await findReminderInDb(id); // Assuming findReminderInDb exists
// if (dbReminder) {
// // Map DB fields to the expected response structure if needed
// return res.status(200).json(dbReminder);
// }
res.status(404).json({ error: `Reminder with ID ${id} not found.` });
}
} catch (error) {
next(error); // Pass DB errors etc. to central handler
}
});
// DELETE /api/reminders/:id - Cancel a pending reminder
router.delete('/:id', (req, res, next) => {
const { id } = req.params;
try {
const cancelled = cancelReminder(id);
if (cancelled) {
res.status(200).json({ message: `Reminder ${id} cancelled successfully.` });
} else {
// Could be not found, or already sent/failed/cancelled
// Check current status to provide a more specific message if needed
const currentStatus = getReminderStatus(id)?.status;
if (currentStatus) {
res.status(400).json({ error: `Reminder ${id} cannot be cancelled (current status: ${currentStatus}).` });
} else {
res.status(404).json({ error: `Reminder with ID ${id} not found.` });
}
// Or simply keep the 404 for ""not found or cannot be cancelled now""
// res.status(404).json({ error: `Reminder with ID ${id} not found or cannot be cancelled.` });
}
} catch (error) {
next(error); // Pass errors to central handler
}
});
module.exports = router;
File: server.js
(Main application setup)
// server.js
require('dotenv').config(); // Ensure env vars are loaded first
const express = require('express');
const reminderRoutes = require('./src/routes/reminderRoutes');
// Optional: Database initialization and rescheduling logic
// const { initializeDatabase, getPendingRemindersFromDb } = require('./src/database');
// const { loadReminderIntoMemory } = require('./src/scheduler'); // Need this if rescheduling from DB
const app = express();
const PORT = process.env.PORT || 3000;
// --- Middleware ---
app.use(express.json()); // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// --- Basic Logging Middleware ---
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// --- Optional Simple API Key Auth Middleware ---
// Uncomment and configure INTERNAL_API_KEY in .env to use
/*
const API_KEY = process.env.INTERNAL_API_KEY;
function authenticateKey(req, res, next) {
const providedKey = req.headers['x-api-key'];
if (!API_KEY) {
console.warn(""Warning: INTERNAL_API_KEY not set, skipping API authentication."");
return next(); // Skip auth if no key configured (for dev/testing)
}
if (providedKey && providedKey === API_KEY) {
next(); // Key is valid
} else {
res.status(401).json({ error: 'Unauthorized: Invalid or missing API Key in X-API-Key header' });
}
}
// Apply authentication to reminder routes
app.use('/api/reminders', authenticateKey, reminderRoutes);
*/
// --- Routes ---
// Apply reminder routes (without auth middleware if commented out above)
app.use('/api/reminders', reminderRoutes);
app.get('/', (req, res) => {
res.send('SMS Reminder Service is running!');
});
// --- 404 Handler ---
// Handles requests that don't match any routes defined above
app.use((req, res, next) => {
res.status(404).json({ error: 'Not Found' });
});
// --- Central Error Handler ---
// Must be defined last, after all routes and other middleware.
// Catches errors passed via next(error).
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
console.error(""Unhandled Error:"", err.stack || err.message); // Log the full error stack
// Respond with a generic 500 error in production
// Avoid leaking detailed error information
res.status(err.status || 500).json({ // Use error status if available, otherwise 500
error: 'Internal Server Error',
// message: process.env.NODE_ENV === 'development' ? err.message : undefined // Optionally show details in dev
});
});
// --- Start Server ---
async function startServer() {
try {
// Optional: Initialize Database and Reschedule Tasks
/*
await initializeDatabase();
console.log(""Database initialized successfully."");
// Reschedule pending tasks from DB on startup
console.log(""Checking for pending reminders in DB to reschedule..."");
const pendingDbReminders = await getPendingRemindersFromDb(); // Fetch pending tasks
let rescheduledCount = 0;
for (const reminder of pendingDbReminders) {
try {
const sendAtDate = new Date(reminder.sendAt);
if (sendAtDate > new Date()) { // Only reschedule if time is still in the future
console.log(`Rescheduling reminder from DB: ${reminder.id} for ${sendAtDate.toISOString()}`);
// --- Requires modification in scheduler.js ---
// Need a function like 'loadReminderIntoMemory' that creates the timeout
// based on DB data without re-inserting into DB, and populates an
// in-memory map of active timeouts (e.g., activeTimeouts<id_ timeoutId>).
loadReminderIntoMemory(reminder.id, reminder.recipient, reminder.message, sendAtDate);
rescheduledCount++;
} else {
console.warn(`Reminder ${reminder.id} from DB has past send time (${reminder.sendAt}), marking as missed.`);
// Optionally update DB status to 'missed' or 'failed_startup'
// await updateReminderStatusInDb(reminder.id, 'missed');
}
} catch (error) {
console.error(`Failed to reschedule reminder ${reminder.id} from DB:`, error);
}
}
console.log(`Rescheduled ${rescheduledCount} pending reminders from the database.`);
*/
// Start listening for HTTP requests
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`API endpoints available at http://localhost:${PORT}/api/reminders`);
console.log(`Current server time: ${new Date().toISOString()} (Ensure server timezone is correct, preferably UTC)`);
});
} catch (error) {
console.error(""Failed to start server:"", error);
process.exit(1);
}
}
startServer();
Explanation:
reminderRoutes.js
:- Defines an Express
Router
. - Includes basic input validation middleware (
validateScheduleRequest
). Usingexpress-validator
is recommended for production. - POST
/
: Schedules a reminder viascheduleReminder
. Returns202 Accepted
withreminderId
. Handles specific scheduling errors. - GET
/:id
: Retrieves status viagetReminderStatus
. Returns404
if not found. Includes comments for optional DB lookup integration. - DELETE
/:id
: Cancels viacancelReminder
. Returns200
on success,404
or400
if not found or not cancellable.
- Defines an Express
server.js
:- Sets up the Express app, loads
dotenv
. - Uses
express.json()
andexpress.urlencoded()
. - Includes simple request logging.
- Includes commented-out example for simple API key authentication.
- Mounts
reminderRoutes
under/api/reminders
. - Defines 404 and central error handling middleware (crucial for catching unexpected errors).
- Contains a
startServer
function with commented-out logic for database initialization and rescheduling on startup (requires Section 5 implementation). - Starts the server and logs relevant information.
- Sets up the Express app, loads
5. (Optional but Recommended) Adding Database Persistence
Using the in-memory map (pendingReminders
) is simple but not reliable; data is lost on restart. A database like SQLite provides persistence.
File: src/database.js
// src/database.js
const sqlite3 = require('sqlite3').verbose(); // Use verbose for detailed logs during dev
require('dotenv').config();
const dbPath = process.env.DATABASE_PATH || './data/reminders.db'; // Default path
// --- Database Connection ---
// Creates the directory and file if they don't exist
const dbDir = require('path').dirname(dbPath);
if (!require('fs').existsSync(dbDir)) {
require('fs').mkdirSync(dbDir, { recursive: true });
}
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error("Error connecting to SQLite database:", err.message);
throw err; // Fail fast if DB connection fails
}
console.log(`Connected to SQLite database at ${dbPath}`);
});
// --- Database Initialization (Schema Setup) ---
/**
* Creates the reminders table if it doesn't exist.
* Should be called once on server startup.
*/
function initializeDatabase() {
return new Promise((resolve, reject) => {
const sql = `
CREATE TABLE IF NOT EXISTS reminders (
id TEXT PRIMARY KEY, -- Unique reminder ID (UUID)
recipient TEXT NOT NULL, -- E.164 phone number
message TEXT NOT NULL,
sendAt TEXT NOT NULL, -- ISO 8601 timestamp string (UTC recommended)
status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'sent', 'failed', 'cancelled', 'missed'
createdAt TEXT NOT NULL, -- ISO 8601 timestamp string
updatedAt TEXT NOT NULL, -- ISO 8601 timestamp string
vonageMessageId TEXT -- Store Vonage message UUID after sending
);
`;
db.run(sql, (err) => {
if (err) {
console.error("Error creating reminders table:", err.message);
return reject(err);
}
console.log("Reminders table checked/created successfully.");
resolve();
});
});
}
// --- Database Operations ---