code examples
code examples
Build a Node.js Appointment Scheduler with SMS Reminders
A step-by-step guide to creating a Node.js and Express application for scheduling appointments with automated Plivo SMS reminders.
This guide provides a step-by-step walkthrough for building a robust appointment scheduling application using Node.js and Express. The application will enable users to book appointments and automatically receive SMS reminders via the Plivo communication platform before their scheduled time.
We'll cover everything from initial project setup and database configuration to implementing core scheduling logic, integrating with Plivo, handling errors, securing the application, and deploying it.
Project Goals:
- Create a web application allowing users to schedule appointments (name, phone number, time).
- Implement a background process to automatically send SMS reminders via Plivo shortly before the appointment time.
- Build a RESTful API for managing appointments.
- Ensure the system is robust, secure, and ready for production deployment.
Technologies Used:
- Node.js: A JavaScript runtime for building server-side applications.
- Express: A minimal and flexible Node.js web application framework.
- Plivo: A cloud communications platform providing SMS APIs.
- MongoDB: A NoSQL database for storing appointment data.
- Mongoose: An Object Data Modeling (ODM) library for MongoDB and Node.js.
- node-cron: A simple cron-like job scheduler for Node.js.
- dotenv: A module to load environment variables from a
.envfile.
System Architecture:
The system consists of the following components:
- Express API Server: Handles incoming HTTP requests to create appointments.
- MongoDB Database: Stores appointment details (name, phone number, time, reminder status).
- Background Scheduler (
node-cron): Periodically checks the database for upcoming appointments that require reminders. - Plivo Service: Interacts with the Plivo API to send SMS reminders.
System Flow:
[User/Client] --(HTTP POST /appointments)--> [Express API Server]
|
+----(Create Appointment)----> [MongoDB Database]
+----(Read/Write Data)------> [MongoDB Database]
[Background Scheduler (node-cron)] --(Periodically Checks)--> [MongoDB Database]
|
+----(Finds Upcoming Appointment)--> [Plivo Service]
| |
| +--(Send SMS Request)--> [Plivo API]
| |
| +--(Sends SMS)--> [User's Phone]
+----(Update Reminder Status)-----> [MongoDB Database]Prerequisites:
- Node.js and npm (or yarn): Installed on your system. (Download: https://nodejs.org/)
- MongoDB: A running MongoDB instance (local or cloud-based like MongoDB Atlas).
- Plivo Account: Sign up for a free Plivo account (https://console.plivo.com/accounts/register/).
- Plivo Auth ID and Auth Token: Found on your Plivo Console dashboard.
- Plivo Phone Number: An SMS-enabled phone number purchased or rented through your Plivo account. This number must be capable of sending messages to your target region(s).
- Basic understanding of JavaScript, Node.js, Express, and REST APIs.
Final Outcome:
By the end of this guide, you will have a functional Node.js application that can:
- Accept appointment bookings via a REST API endpoint.
- Store appointment data securely.
- Automatically send SMS reminders at a configurable interval before the appointment time using Plivo.
- Be structured and configured for potential deployment.
1. Setting up the project
Let's initialize our Node.js project, install necessary dependencies, and set up the basic project structure and environment configuration.
1.1 Create Project Directory:
Open your terminal or command prompt and create a new directory for the project.
mkdir plivo-scheduler-app
cd plivo-scheduler-app1.2 Initialize Node.js Project:
Initialize the project using npm, which creates a package.json file.
npm init -y- Why
-y? This flag automatically accepts the default settings for the project initialization, speeding up the process.
1.3 Install Dependencies:
Install the core dependencies: Express for the web server, Mongoose for database interaction, Plivo's Node.js SDK, node-cron for scheduling, and dotenv for environment variables.
npm install express mongoose plivo node-cron dotenvexpress: The web framework.mongoose: ODM for MongoDB, simplifying data modeling and interaction.plivo: The official Plivo Node.js helper library.node-cron: Task scheduler used to trigger reminder checks.dotenv: Loads environment variables from a.envfile intoprocess.env.
(Optional) Install Development Dependencies:
Install nodemon for automatic server restarts during development.
npm install --save-dev nodemonAdd a development script to your package.json:
// package.json
{
// ... other properties
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
// ... other properties
}- Why
nodemon? It watches for file changes in your project and automatically restarts the Node.js application, significantly improving the development workflow.
1.4 Project Structure:
Create the following directory structure within plivo-scheduler-app:
plivo-scheduler-app/
├── config/
│ └── db.js # Database connection logic
├── controllers/
│ └── appointmentController.js # Handles request logic for appointments
├── models/
│ └── Appointment.js # Mongoose schema for appointments
├── routes/
│ └── appointments.js # Defines API routes for appointments
├── services/
│ ├── plivoService.js # Logic for interacting with Plivo API
│ └── scheduler.js # Contains the node-cron job logic
├── .env # Environment variables (DO NOT COMMIT)
├── .gitignore # Specifies intentionally untracked files
├── package.json
├── package-lock.json
└── server.js # Main application entry point
- Why this structure? This separation of concerns (config, controllers, models, routes, services) makes the application modular, easier to understand, maintain, and scale.
1.5 Configure Environment Variables (.env):
Create a file named .env in the project root. Add your Plivo credentials, MongoDB connection string, and Plivo phone number.
# .env
# Plivo Credentials
# Get these from your Plivo Console: https://console.plivo.com/dashboard/
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
PLIVO_PHONE_NUMBER=+1XXXXXXXXXX # Your Plivo SMS-enabled number in E.164 format
# MongoDB Connection
MONGO_URI=mongodb://localhost:27017/appointment_scheduler # Replace with your MongoDB connection string
# Application Settings
PORT=3000
REMINDER_MINUTES_BEFORE=60 # Send reminder 60 minutes before appointment
CRON_SCHEDULE=* * * * * # Run scheduler check every minute- Obtaining Plivo Credentials: Log in to your Plivo account. Your Auth ID and Auth Token are displayed prominently on the main dashboard overview page.
- Obtaining Plivo Number: Navigate to
Phone Numbers>Your Numbersin the Plivo console. Ensure the number you use is SMS enabled. Use the E.164 format (e.g.,+14155551212). - MongoDB URI: If using a local MongoDB instance, the default URI is usually
mongodb://localhost:27017/your_db_name. For cloud providers like MongoDB Atlas, copy the connection string provided by the service. - Security: The
.envfile contains sensitive credentials. Never commit this file to version control.
1.6 Configure .gitignore:
Create a .gitignore file in the project root to prevent committing sensitive files and unnecessary directories.
# .gitignore
# Dependencies
node_modules/
# Environment variables
.env
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS generated files
.DS_Store
Thumbs.db1.7 Set up Basic Express Server (server.js):
Create the main entry point for your application.
// server.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const connectDB = require('./config/db');
const appointmentRoutes = require('./routes/appointments');
const { startScheduler } = require('./services/scheduler');
const mongoose = require('mongoose'); // Needed for health check later
const app = express();
// Connect to Database
connectDB();
// Init Middleware
app.use(express.json()); // Enable parsing JSON request bodies
// Define Routes
app.get('/', (req, res) => res.send('Appointment Scheduler API Running'));
app.use('/api/appointments', appointmentRoutes); // Mount appointment routes
// Health Check Endpoint (Example)
app.get('/healthz', async (req, res) => {
try {
// Check database connection status (Mongoose connection state)
const dbState = mongoose.connection.readyState;
// 0: disconnected; 1: connected; 2: connecting; 3: disconnecting
if (dbState === 1) {
res.status(200).send('OK');
} else {
throw new Error(`DB not connected. State: ${dbState}`);
}
} catch (error) {
console.error("Health check failed:", error.message);
res.status(503).send('Service Unavailable');
}
});
const PORT = process.env.PORT || 3000;
// Basic Error Handling (Example - Enhance as needed)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
// Start server only if not in test environment (to allow Supertest to bind port)
// And export app for testing purposes
if (process.env.NODE_ENV !== 'test') {
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
// Start the reminder scheduler after the server starts
startScheduler();
});
}
module.exports = app; // Export the app instance for testingrequire('dotenv').config(): This line must be at the top to ensure environment variables are loaded before other modules need them.connectDB(): Function to establish the MongoDB connection (we'll create this next).express.json(): Middleware to parse incoming JSON payloads./api/appointments: Base path for all appointment-related API endpoints.startScheduler(): Function to initialize thenode-cronjob (created later).module.exports = app;: Exports the Express app instance, which is necessary for integration testing frameworks like Supertest. Note the conditionalapp.listento prevent the server from starting automatically during tests.
1.8 Set up Database Connection (config/db.js):
Create the logic to connect to your MongoDB instance using Mongoose.
// config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
// Mongoose 6+ requires MONGO_URI to be defined
if (!process.env.MONGO_URI) {
throw new Error('MONGO_URI is not defined in environment variables.');
}
await mongoose.connect(process.env.MONGO_URI, {
// Mongoose 6+ uses these defaults, but explicit for clarity
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB Connected...');
} catch (err) {
console.error('MongoDB Connection Error:', err.message);
// Exit process with failure
process.exit(1);
}
};
module.exports = connectDB;mongoose.connect(): Attempts to connect to the MongoDB instance specified by theMONGO_URIenvironment variable. Includes a check that the URI is defined.- Error Handling: Logs connection errors and exits the application if the database connection fails, as the application likely cannot function without it.
Now you have a basic project structure, dependencies installed, and a simple Express server ready. You can test this basic setup by running:
npm run dev # Uses nodemon if installed
# or
npm start # Uses node directlyYou should see "Server started on port 3000" and "MongoDB Connected..." in your console. Accessing http://localhost:3000/ in your browser should show "Appointment Scheduler API Running". Accessing http://localhost:3000/healthz should show "OK".
2. Implementing core functionality: Appointments
Now let's define the data structure for our appointments and create the core logic for managing them.
2.1 Define the Appointment Schema (models/Appointment.js):
Use Mongoose to define the structure of appointment documents in MongoDB.
// models/Appointment.js
const mongoose = require('mongoose');
const AppointmentSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Patient name is required.'],
trim: true,
},
phoneNumber: {
type: String,
required: [true, 'Phone number is required.'],
// Example: Very basic check for '+' prefix and digits. Doesn't enforce length or full E.164 rules.
// Use a library like `google-libphonenumber` for production validation.
validate: {
validator: function(v) {
return /^\+\d+$/.test(v);
},
message: props => `${props.value} is not a valid phone number format (must start with + and contain digits). Use E.164 format.`
},
},
appointmentTime: {
type: Date,
required: [true, 'Appointment time is required.'],
},
timeZone: {
type: String,
required: [true, 'Time zone is required (e.g., America/New_York).'], // Store the original time zone
},
reminderSent: {
type: Boolean,
default: false, // Track if a reminder has been sent
},
}, { timestamps: true }); // Enable timestamps
// Index appointmentTime for faster querying by the scheduler
AppointmentSchema.index({ appointmentTime: 1, reminderSent: 1 });
module.exports = mongoose.model('Appointment', AppointmentSchema);required: Fields marked as required must be provided.trim: Removes leading/trailing whitespace from strings.phoneNumberValidation: Includes a very basic regex validator. It checks for a leading+followed by digits but doesn't enforce length rules or other E.164 constraints. For production, using a dedicated library likegoogle-libphonenumberis strongly recommended for robust validation.appointmentTime(Date): Stores the appointment date and time. Crucially, Mongoose stores dates in UTC format by default. This is good practice.timeZone(String): Stores the original time zone provided by the user (e.g., 'America/New_York', 'Europe/London'). This is important for accurately calculating reminder times and formatting messages if needed. You can use libraries likemoment-timezoneto validate time zone strings.reminderSent(Boolean): A flag to prevent sending multiple reminders for the same appointment.timestamps: true: Automatically addscreatedAtandupdatedAtfields.index: Creates a database index onappointmentTimeandreminderSent. This significantly speeds up the scheduler's queries when searching for appointments needing reminders.
2.2 Implement Appointment Controller (controllers/appointmentController.js):
Create functions to handle the logic for creating and potentially retrieving appointments.
// controllers/appointmentController.js
const Appointment = require('../models/Appointment');
// @desc Create a new appointment
// @route POST /api/appointments
// @access Public (Add auth later if needed)
exports.createAppointment = async (req, res, next) => {
try {
const { name, phoneNumber, appointmentTime, timeZone } = req.body;
// Input validation should ideally be handled by middleware (e.g., express-validator).
// Basic checks can remain as a fallback if needed.
// if (!name || !phoneNumber || !appointmentTime || !timeZone) {
// return res.status(400).json({ success: false, message: 'Missing required fields.' });
// }
// Create appointment (Mongoose handles Date conversion if string is ISO 8601 format)
const newAppointment = await Appointment.create({
name,
phoneNumber,
appointmentTime, // Expecting ISO 8601 format string e.g., ""2025-12-31T14:30:00.000Z"" or a JS Date object
timeZone,
reminderSent: false // Explicitly set, though it's the default
});
console.log(`Appointment created: ${newAppointment._id} for ${newAppointment.name} at ${newAppointment.appointmentTime}`);
res.status(201).json({
success: true,
data: newAppointment,
message: 'Appointment scheduled successfully.'
});
} catch (error) {
console.error('Error creating appointment:', error);
// Handle Mongoose validation errors specifically
if (error.name === 'ValidationError') {
const messages = Object.values(error.errors).map(val => val.message);
return res.status(400).json({ success: false, message: messages.join(', ') });
}
// Generic error handling
res.status(500).json({ success: false, message: 'Server error creating appointment.' });
// Optional: pass error to a global error handler `next(error)`
}
};
// @desc Get all appointments (Example - add filtering/pagination for production)
// @route GET /api/appointments
// @access Public (Add auth later if needed)
exports.getAppointments = async (req, res, next) => {
try {
// Example: Add basic pagination
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 10;
const skip = (page - 1) * limit;
const appointments = await Appointment.find()
.sort({ appointmentTime: 1 }) // Sort by date
.skip(skip)
.limit(limit);
const totalAppointments = await Appointment.countDocuments(); // Get total count for pagination info
res.status(200).json({
success: true,
count: appointments.length,
pagination: {
currentPage: page,
totalPages: Math.ceil(totalAppointments / limit),
totalItems: totalAppointments
},
data: appointments
});
} catch (error) {
console.error('Error fetching appointments:', error);
res.status(500).json({ success: false, message: 'Server error fetching appointments.' });
}
};
// Add functions for getting single appointment, updating, deleting as neededcreateAppointment: Extracts data from the request body, relies on middleware for primary validation (recommended), creates a newAppointmentdocument using the Mongoose model, and returns the created appointment. Includes fallback handling for potential Mongoose validation errors.getAppointments: Retrieves appointments, now including basic pagination (page,limitquery parameters) and sorting.- Time Handling: Assumes the
appointmentTimein the request body is either a JavaScript Date object or an ISO 8601 formatted string (like2025-05-15T10:00:00.000Z). Mongoose will automatically convert this to a UTC Date object for storage.
2.3 Database Further Considerations:
- Migrations: For evolving schemas in production, use a migration library like
migrate-mongooseor handle schema changes manually with scripts. - Data Population: For testing, you could create scripts to populate the database with sample appointments using the Mongoose model.
3. Building the API Layer
Define the actual HTTP routes that map to the controller functions.
3.1 Define Appointment Routes (routes/appointments.js):
Use Express Router to define the endpoints. We'll add validation middleware later in the Security section.
// routes/appointments.js
const express = require('express');
const { createAppointment, getAppointments } = require('../controllers/appointmentController');
// We will add validation middleware here later (e.g., using express-validator)
// const { validateAppointment } = require('../middleware/validator');
const router = express.Router();
// Route for creating an appointment
// Apply validation middleware before the controller (example shown, implement later)
// router.post('/', validateAppointment, createAppointment);
router.post('/', createAppointment); // Currently without validation middleware
// Route for getting all appointments
router.get('/', getAppointments);
// Add routes for GET /:id, PUT /:id, DELETE /:id as needed
module.exports = router;- This file links the
POST /api/appointmentsrequest path to thecreateAppointmentcontroller function andGET /api/appointmentstogetAppointments. - We've noted where validation middleware should ideally be added.
Testing the Endpoint:
You can now test creating an appointment using curl or a tool like Postman.
Using curl:
curl -X POST http://localhost:3000/api/appointments \
-H "Content-Type: application/json" \
-d '{
"name": "Alice Smith",
"phoneNumber": "+15551234567",
"appointmentTime": "2025-12-25T15:00:00.000Z",
"timeZone": "America/New_York"
}'Expected Response (JSON):
{
"success": true,
"data": {
"name": "Alice Smith",
"phoneNumber": "+15551234567",
"appointmentTime": "2025-12-25T15:00:00.000Z",
"timeZone": "America/New_York",
"reminderSent": false,
"_id": "someGeneratedMongoId",
"createdAt": "...",
"updatedAt": "..."
},
"message": "Appointment scheduled successfully."
}Check your MongoDB database to confirm the appointment was saved correctly. Also check your server console logs. Try the GET endpoint too: curl http://localhost:3000/api/appointments?limit=5
4. Integrating with Plivo for SMS Reminders
Now, let's integrate the Plivo API to enable sending SMS reminders.
4.1 Create Plivo Service (services/plivoService.js):
This service encapsulates all interactions with the Plivo API.
// services/plivoService.js
const plivo = require('plivo');
require('dotenv').config(); // Ensure env vars are loaded
// Input Validation (Basic)
if (!process.env.PLIVO_AUTH_ID || !process.env.PLIVO_AUTH_TOKEN || !process.env.PLIVO_PHONE_NUMBER) {
console.error(""Plivo credentials or phone number missing in .env file."");
// In a real app, you might want to prevent the app from starting
// or disable scheduling if Plivo isn't configured.
// For now, we log the error. The client initialization will likely fail later.
}
// Initialize Plivo client only if credentials exist
let client;
if (process.env.PLIVO_AUTH_ID && process.env.PLIVO_AUTH_TOKEN) {
client = new plivo.Client(process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN);
} else {
console.warn(""Plivo client not initialized due to missing credentials."");
}
const plivoPhoneNumber = process.env.PLIVO_PHONE_NUMBER;
/**
* Sends an SMS message using the Plivo API.
* @param {string} to - The recipient's phone number in E.164 format.
* @param {string} text - The message content.
* @returns {Promise<object>} - The Plivo API response.
* @throws {Error} - If sending fails or Plivo client isn't initialized.
*/
const sendSms = async (to, text) => {
if (!client) {
throw new Error(""Plivo client is not initialized. Cannot send SMS."");
}
if (!plivoPhoneNumber) {
throw new Error(""Plivo source phone number (PLIVO_PHONE_NUMBER) is not configured."");
}
if (!to || !text) {
throw new Error(""Recipient phone number (to) and message text are required."");
}
console.log(`Attempting to send SMS via Plivo from ${plivoPhoneNumber} to ${to}`);
try {
const response = await client.messages.create(
plivoPhoneNumber, // src
to, // dst
text // text
// Optional parameters (e.g., { url: 'status_callback_url' }) can be added here
);
// Ensure response structure is handled safely
const messageUuid = response && response.messageUuid ? response.messageUuid[0] : 'N/A';
console.log(`Plivo SMS Response: Message UUID ${messageUuid}`);
// Plivo response includes message_uuid which is useful for tracking
return response;
} catch (error) {
console.error(""Plivo API Error:"", error);
// Rethrow or handle specific Plivo errors (e.g., invalid number format, insufficient credits)
throw new Error(`Failed to send SMS via Plivo: ${error.message}`);
}
};
module.exports = {
sendSms,
// Export client if needed elsewhere, but usually better to keep interactions within the service
};- Initialization: Reads Plivo credentials and the source phone number from environment variables. It includes checks to ensure these are present and warns if the client cannot be initialized.
sendSmsFunction: Takes the recipient number (to) and messagetextas arguments. It performs basic checks, then usesclient.messages.create()to send the SMS.- Error Handling: Includes a
try...catchblock to handle errors during the API call (e.g., network issues, invalid credentials, invalid recipient number, insufficient funds). It logs the error and throws a new error for the calling function (the scheduler) to handle. Logs the Message UUID from the response for tracking. - Security: Relies on
dotenvto load credentials, keeping them out of the codebase.
4.2 How to Obtain Plivo Credentials:
- Log in to your Plivo Console.
- Your Auth ID and Auth Token are displayed on the main dashboard overview page. Click the ""eye"" icon to reveal the Auth Token if it's hidden.
- Copy these values and paste them into your
.envfile forPLIVO_AUTH_IDandPLIVO_AUTH_TOKEN. - Navigate to Phone Numbers > Your Numbers.
- Copy the SMS-enabled number you want to use as the sender, ensuring it's in E.164 format (e.g.,
+14155551212), and paste it into your.envfile forPLIVO_PHONE_NUMBER.
5. Implementing the Reminder Scheduler
Let's use node-cron to periodically check for appointments needing reminders and trigger the Plivo SMS service.
5.1 Create Scheduler Logic (services/scheduler.js):
This section uses the moment-timezone library for reliable time zone handling. Install it if you haven't already:
npm install moment-timezoneNow, create the scheduler file:
// services/scheduler.js
const cron = require('node-cron');
const Appointment = require('../models/Appointment');
const { sendSms } = require('./plivoService'); // Use the Plivo service
const moment = require('moment-timezone'); // Use moment-timezone for reliable time zone handling
require('dotenv').config();
const CRON_SCHEDULE = process.env.CRON_SCHEDULE || '* * * * *'; // Default: every minute
const REMINDER_MINUTES_BEFORE = parseInt(process.env.REMINDER_MINUTES_BEFORE || '60', 10);
let task = null; // To hold the cron task instance
let isJobRunning = false; // Simple lock to prevent overlap
/**
* Checks for appointments needing reminders and sends SMS via Plivo.
*/
const checkAndSendReminders = async () => {
if (isJobRunning) {
console.log(`[${new Date().toISOString()}] Reminder check skipped: Previous job still running.`);
return;
}
isJobRunning = true;
console.log(`[${new Date().toISOString()}] Running reminder check...`);
// Calculate the time window for reminders
const now = moment.utc(); // Work with UTC time
const reminderWindowStart = now.clone(); // Avoid modifying 'now' directly
const reminderWindowEnd = now.clone().add(REMINDER_MINUTES_BEFORE, 'minutes');
console.log(`Checking for appointments between ${reminderWindowStart.toISOString()} and ${reminderWindowEnd.toISOString()}`);
try {
// Find appointments within the window that haven't had a reminder sent
const appointmentsToRemind = await Appointment.find({
appointmentTime: {
$gte: reminderWindowStart.toDate(), // Greater than or equal to now
$lte: reminderWindowEnd.toDate() // Less than or equal to X minutes from now
},
reminderSent: false // Only find those not yet reminded
}).lean(); // Use .lean() for performance if not modifying Mongoose docs directly
console.log(`Found ${appointmentsToRemind.length} appointments needing reminders.`);
if (appointmentsToRemind.length === 0) {
isJobRunning = false; // Release lock
return; // Nothing to do
}
// Process reminders sequentially to avoid overwhelming Plivo API or database
for (const appointment of appointmentsToRemind) {
console.log(`Processing reminder for appointment ID: ${appointment._id}`);
// Format the appointment time for the SMS message *using the stored time zone*
let localAppointmentTime = 'N/A';
try {
localAppointmentTime = moment(appointment.appointmentTime) // This is UTC
.tz(appointment.timeZone) // Convert to user's timezone
.format('h:mm A z'); // e.g., "3:00 PM EDT"
} catch (tzError) {
console.error(`Error formatting time for appointment ${appointment._id} with timezone ${appointment.timeZone}:`, tzError);
// Use UTC time as fallback or skip? For now, log and continue.
localAppointmentTime = moment(appointment.appointmentTime).utc().format('h:mm A [UTC]');
}
const messageText = `Hi ${appointment.name}, this is a reminder for your appointment scheduled at ${localAppointmentTime}.`;
try {
// Send SMS using the Plivo service
await sendSms(appointment.phoneNumber, messageText);
console.log(`Reminder SMS sent successfully to ${appointment.phoneNumber} for appointment ${appointment._id}`);
// IMPORTANT: Mark the appointment as reminderSent in the database
await Appointment.updateOne({ _id: appointment._id }, { $set: { reminderSent: true } });
console.log(`Marked appointment ${appointment._id} as reminderSent.`);
} catch (smsError) {
console.error(`Failed to send reminder or update status for appointment ${appointment._id}:`, smsError);
// Decide on retry logic:
// - Log the error and move on (simplest).
// - Implement a retry count on the appointment document.
// - Push failed jobs to a separate queue for later processing.
// For this guide, we'll log and continue.
}
// Optional: Add a small delay between sends if hitting rate limits
// await new Promise(resolve => setTimeout(resolve, 200)); // 200ms delay
}
} catch (dbError) {
console.error('Error querying appointments for reminders:', dbError);
} finally {
isJobRunning = false; // Release lock regardless of outcome
console.log(`[${new Date().toISOString()}] Reminder check finished.`);
}
};
/**
* Starts the cron job.
*/
const startScheduler = () => {
// Validate cron schedule format (basic check)
if (!cron.validate(CRON_SCHEDULE)) {
console.error(`Invalid CRON_SCHEDULE format: "${CRON_SCHEDULE}". Scheduler not started.`);
return;
}
console.log(`Scheduler starting with schedule: ${CRON_SCHEDULE}`);
// Schedule the task
task = cron.schedule(CRON_SCHEDULE, checkAndSendReminders, {
scheduled: true,
// Optional: specify timezone for the cron schedule itself if needed,
// but the logic inside checkAndSendReminders uses UTC/moment-timezone.
// timezone: "America/New_York"
});
// Optional: You can add listeners for job start/stop if needed
// task.start(); // Already starts if scheduled: true
console.log("Reminder scheduler job registered.");
};
/**
* Stops the cron job gracefully.
*/
const stopScheduler = () => {
if (task) {
console.log("Stopping reminder scheduler...");
task.stop();
task = null;
console.log("Scheduler stopped.");
}
};
// Export functions
module.exports = {
startScheduler,
stopScheduler,
checkAndSendReminders // Export for potential manual triggering or testing
};- Dependencies: Requires
node-cron, theAppointmentmodel, theplivoService, andmoment-timezone. - Configuration: Reads
CRON_SCHEDULEandREMINDER_MINUTES_BEFOREfrom.env. checkAndSendReminders:- Uses a simple
isJobRunningflag to prevent overlapping executions if a check takes longer than the interval. - Calculates the time window (now to
REMINDER_MINUTES_BEFOREfrom now) in UTC usingmoment.utc(). - Queries MongoDB for appointments within this window where
reminderSentisfalse. Uses.lean()for better performance as we only need to read data. - Iterates through found appointments.
- Time Zone Handling: Uses
moment(appointment.appointmentTime).tz(appointment.timeZone).format(...)to format the appointment time correctly in the user's specified time zone for the SMS message. Includes basic error handling for invalid time zones. - Calls
sendSmsfrom theplivoService. - Crucially: Updates the appointment document in the database, setting
reminderSenttotrueafter successfully sending the SMS to prevent duplicates. - Includes error handling for both SMS sending and database updates.
- Uses a simple
startScheduler:- Validates the
CRON_SCHEDULEformat usingcron.validate(). - Uses
cron.schedule()to set up the recurring job, callingcheckAndSendRemindersbased on the schedule. - Stores the task instance to allow stopping it later.
- Validates the
stopScheduler: Provides a function to gracefully stop the scheduled task if needed (e.g., during application shutdown).- Integration: The
startSchedulerfunction is called inserver.jsafter the server successfully starts.
Frequently Asked Questions
When should I use moment-timezone in Node.js?
Use moment-timezone when accurate time zone handling and conversion are needed. This is essential for the appointment reminder project to ensure the correct time is displayed in the SMS message regardless of the user's location.
How to structure a Node.js Express project?
Organize code into modular components like config, controllers, models, routes, and services, as seen in the project structure example. This promotes maintainability and scalability.
Can I use a different SMS provider?
While this guide uses Plivo, you could adapt it to use other SMS providers by using their respective Node.js SDKs and updating the Plivo-specific code in the 'services/plivoService.js' file.
How to set up Plivo for SMS reminders?
First, sign up for a Plivo account and obtain your Auth ID and Auth Token from the Plivo console dashboard. Purchase an SMS-enabled Plivo phone number and ensure it's configured to send messages to your target region. Save these credentials and your Plivo number securely in your project's .env file.
How to schedule SMS reminders with Node.js?
Use the node-cron library to schedule a recurring task. This task should query your database for upcoming appointments, then use the Plivo Node.js SDK to send SMS messages to the appropriate phone numbers.
What is Plivo used for in this project?
Plivo is a cloud communications platform that provides the SMS API used to send appointment reminders. The application interacts with the Plivo API through the Plivo Node.js SDK.
What database is used for appointment scheduling?
MongoDB is used as the database, with Mongoose serving as the Object Data Modeling (ODM) library. Mongoose simplifies interaction with MongoDB and allows schema definition for appointments.
How to handle time zones for appointment reminders?
Store the user's time zone information in the database along with the UTC appointment time. Use moment-timezone or a similar library to convert the UTC appointment time into the user's time zone when formatting the reminder message.
What Node.js frameworks are used in the appointment scheduler?
The project uses Express.js as the web application framework and Node.js as the runtime environment. Express.js provides a minimal and flexible foundation for building the API server.
How to prevent duplicate SMS reminder messages?
Include a 'reminderSent' boolean field in your appointment data model. After successfully sending a reminder, set this field to 'true' in the database. This will prevent future checks from sending additional reminders.
Why does the app use dotenv?
The dotenv module is used to load environment variables from a .env file. This allows you to store sensitive information, like API keys and database credentials, separately from your code.
What is node-cron and what's its purpose?
Node-cron is a task scheduler for Node.js that allows you to run tasks on a predefined schedule, similar to cron jobs in Unix-like systems. It's used here to periodically trigger the reminder check and send SMS reminders before appointments.
How to install required project dependencies?
Run 'npm install express mongoose plivo node-cron dotenv moment-timezone' in your project directory to install all the required dependencies. Optionally, install 'nodemon' as a development dependency for automatic server restarts.
How to run the Node.js scheduler application?
After setting up the project and installing dependencies, run 'npm run dev' (with nodemon) or 'npm start' to start the application. The reminder scheduler will start automatically after the server starts.