code examples
code examples
Build Scheduled SMS Reminders with Node.js, Express, and Plivo
A guide on creating a Node.js application using Express, MongoDB, and Plivo to schedule and send automated SMS appointment reminders.
This guide walks you through building a robust Node.js and Express application to schedule and send SMS appointment reminders using the Plivo communication platform. We'll cover everything from project setup and core logic to deployment and monitoring, creating a production-ready system.
By the end of this tutorial, you will have a fully functional application that enables users (or an admin) to create appointments and automatically sends SMS reminders via Plivo shortly before the scheduled time. This solves the common business need of reducing no-shows and keeping customers informed.
Project Overview and Goals
What we'll build:
- An Express.js web application with API endpoints to create appointments.
- A MongoDB database to store appointment details (name, phone number, time, timezone).
- A background scheduling mechanism using
node-cronto periodically check for upcoming appointments. - Integration with the Plivo SMS API to send reminders to the specified phone numbers.
- Robust error handling, logging, and basic security measures.
Technologies Involved:
- Node.js: A JavaScript runtime for building the backend server.
- Express.js: A minimal and flexible Node.js web application framework for creating the API.
- MongoDB: A NoSQL database for storing appointment data. We'll use Mongoose as the ODM (Object Data Modeling) library.
- Plivo: A cloud communications platform providing SMS APIs. We'll use their Node.js SDK.
node-cron: A simple cron-like job scheduler for Node.js.dotenv: To manage environment variables securely.winston: For robust logging.express-validator: For request validation.cors: To enable Cross-Origin Resource Sharing.express-rate-limit: To protect against brute-force attacks.luxon: For robust timezone validation.date-fns: For date formatting.date-fns-tz: For timezone conversions.
Why these technologies?
- Node.js and Express provide a fast, scalable, and widely-used foundation for building APIs.
- MongoDB offers flexibility for storing appointment data, and Mongoose simplifies interactions.
- Plivo provides reliable SMS delivery APIs with clear documentation and a helpful Node.js SDK.
node-cronis straightforward for implementing background scheduling tasks within the Node.js application itself.
System Architecture:
graph LR
A[Client/Admin UI] -- HTTP POST --> B(Express API);
B -- Create Appointment --> C{MongoDB Database};
D(Node Cron Scheduler) -- Runs Every Minute --> E{Reminder Service};
E -- Check Upcoming Appointments --> C;
E -- Found Appointment --> F(Plivo Service);
F -- Send SMS --> G((Plivo API));
G -- SMS Delivered --> H(User's Phone);
subgraph Node.js/Express Application
B
D
E
F
end
style C fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#ccf,stroke:#333,stroke-width:2px(Note: Mermaid diagram rendering depends on the Markdown platform.)
Prerequisites:
- Node.js (v16 or later recommended) and npm/yarn installed.
- Access to a MongoDB instance (local installation or a cloud service like MongoDB Atlas).
- A Plivo account (free trial available).
- A Plivo phone number capable of sending SMS messages.
- Basic understanding of JavaScript, Node.js, Express, and REST APIs.
- A code editor (like VS Code).
- A tool for testing APIs (like Postman or
curl).
1. Setting up the Project
Let's initialize the project, install dependencies, and set up the basic structure.
-
Create Project Directory:
bashmkdir plivo-scheduler-app cd plivo-scheduler-app -
Initialize Node.js Project:
bashnpm init -yThis creates a
package.jsonfile. -
Install Dependencies:
bashnpm install express mongoose plivo node-cron dotenv winston express-validator cors express-rate-limit luxon date-fns date-fns-tzexpress: Web framework.mongoose: MongoDB ODM.plivo: Plivo Node.js SDK.node-cron: Job scheduler.dotenv: Environment variable loader.winston: Logger.express-validator: Input validation middleware.cors: Enable CORS.express-rate-limit: API rate limiting.luxon: Timezone validation.date-fns: Date formatting.date-fns-tz: Timezone conversion.
-
Install Development Dependencies (Optional but Recommended):
bashnpm install --save-dev nodemon jest supertestnodemon: Automatically restarts the server during development.jest: Testing framework.supertest: HTTP assertion library for testing API endpoints.
-
Configure
nodemon(Optional): Add adevscript to yourpackage.json:json// package.json { // ... other fields ""scripts"": { ""start"": ""node server.js"", ""dev"": ""nodemon server.js"", ""test"": ""jest"" }, // ... other fields }Now you can run
npm run devto start the server with auto-reloading. -
Create Environment File: Create a file named
.envin the project root. Never commit this file to version control.dotenv# .env PORT=5000 MONGO_URI=mongodb://localhost:27017/plivoScheduler # Replace with your MongoDB connection string PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN PLIVO_PHONE_NUMBER=+1XXXXXXXXXX # Your Plivo phone number in E.164 formatPORT: The port your Express server will listen on.MONGO_URI: Your MongoDB connection string.PLIVO_AUTH_ID&PLIVO_AUTH_TOKEN: Found on your Plivo Console dashboard. Go toAccount -> Overviewafter logging in.PLIVO_PHONE_NUMBER: A Plivo phone number you own, enabled for SMS, in E.164 format (e.g.,+14155551212). You can purchase one underPhone Numbers -> Buy Numbersin the Plivo console.
-
Create
.gitignoreFile: Create a.gitignorefile in the root to prevent sensitive files and unnecessary directories from being committed.text# .gitignore node_modules/ .env npm-debug.log *.log -
Set Up Project Structure: Create the following directories for better organization:
plivo-scheduler-app/ ├── config/ # Configuration files (DB, logger) ├── controllers/ # Request handlers ├── models/ # Mongoose models/schemas ├── routes/ # Express route definitions ├── services/ # Business logic (Plivo, scheduling) ├── utils/ # Utility functions ├── logs/ # Log files (ensure this exists or winston might error) ├── tests/ # Test files ├── .env ├── .gitignore ├── package.json └── server.js # Main application entry pointCreate an empty file inside
logs/like.gitkeepif you want Git to track the empty directory. -
Create Initial Server File (
server.js):javascript// server.js require('dotenv').config(); // Load environment variables first const express = require('express'); const cors = require('cors'); const connectDB = require('./config/db'); const logger = require('./config/logger'); const appointmentRoutes = require('./routes/appointments'); const errorHandler = require('./utils/errorHandler'); const startScheduler = require('./services/scheduler'); // Connect to Database connectDB(); const app = express(); // --- Middleware --- // Enable CORS for all origins (adjust for production) app.use(cors()); // Body Parser Middleware app.use(express.json()); app.use(express.urlencoded({ extended: false })); // Logging Middleware (Example: Log requests) app.use((req, res, next) => { logger.info(`${req.method} ${req.originalUrl}`, { ip: req.ip }); next(); }); // --- API Routes --- app.use('/api/appointments', appointmentRoutes); // Simple Health Check Endpoint app.get('/health', (req, res) => res.status(200).send('OK')); // --- Error Handling Middleware --- // Should be loaded after routes // Assuming errorHandler is defined in './utils/errorHandler.js' // Example basic error handler if not defined elsewhere: // app.use((err, req, res, next) => { // logger.error(err.message, { stack: err.stack }); // res.status(err.statusCode || 500).json({ // success: false, // error: err.message || 'Server Error' // }); // }); // Replace the above comment with: app.use(errorHandler); const PORT = process.env.PORT || 5000; const server = app.listen(PORT, () => { // Store server instance logger.info(`Server running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`); // Start the scheduler after the server starts startScheduler(); logger.info('Reminder scheduler started.'); }); // Handle unhandled promise rejections process.on('unhandledRejection', (err, promise) => { logger.error(`Unhandled Rejection: ${err.message}`, { stack: err.stack }); // Optionally close server gracefully // server.close(() => process.exit(1)); }); // Handle uncaught exceptions process.on('uncaughtException', (err) => { logger.error(`Uncaught Exception: ${err.message}`, { stack: err.stack }); // Optionally close server gracefully // server.close(() => process.exit(1)); });
2. Implementing Core Functionality
Now, let's build the scheduling and reminder sending logic.
-
Create Logger Configuration (
config/logger.js):javascript// config/logger.js const winston = require('winston'); const path = require('path'); const logFormat = winston.format.printf(({ level, message, timestamp, stack, ...metadata }) => { let log = `${timestamp} [${level}]: ${message}`; // Add metadata if present if (metadata && Object.keys(metadata).length > 0) { log += ` ${JSON.stringify(metadata)}`; } // Add stack trace for errors if (stack) { log += `\nStack: ${stack}`; } return log; }); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), // Log stack traces logFormat ), transports: [ // Console transport new winston.transports.Console(), // File transport for all logs new winston.transports.File({ filename: path.join('logs', 'app.log'), level: 'info', format: winston.format.combine( winston.format.uncolorize(), // Don't write color codes to file winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), logFormat ), }), // File transport for error logs new winston.transports.File({ filename: path.join('logs', 'error.log'), level: 'error', format: winston.format.combine( winston.format.uncolorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), logFormat ), }), ], exceptionHandlers: [ // Catch unhandled exceptions new winston.transports.File({ filename: path.join('logs', 'exceptions.log') }) ], rejectionHandlers: [ // Catch unhandled promise rejections new winston.transports.File({ filename: path.join('logs', 'rejections.log') }) ] }); module.exports = logger;Why Winston? It provides flexible logging with multiple transports (console, file), levels, and formatting, essential for production monitoring.
-
Create Plivo Service (
services/plivoService.js): This service encapsulates interaction with the Plivo API.javascript// services/plivoService.js const plivo = require('plivo'); const logger = require('../config/logger'); const client = new plivo.Client(process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN); /** * Sends an SMS reminder using Plivo. * @param {string} toPhoneNumber - Recipient phone number in E.164 format. * @param {string} messageBody - The text message content. * @returns {Promise<object>} Plivo API response. * @throws {Error} If sending fails or inputs are invalid. */ const sendSmsReminder = async (toPhoneNumber, messageBody) => { // Basic validation (more robust validation in API layer) if (!toPhoneNumber || !messageBody) { throw new Error('Recipient phone number and message body are required.'); } // Strict E.164 format check before sending to Plivo if (!/^\+[1-9]\d{1,14}$/.test(toPhoneNumber)) { throw new Error(`Invalid E.164 format for recipient phone number: ${toPhoneNumber}`); } logger.info(`Attempting to send SMS to ${toPhoneNumber}`); try { const response = await client.messages.create( process.env.PLIVO_PHONE_NUMBER, // Sender (Your Plivo Number) toPhoneNumber, // Recipient messageBody // Message Text // Optional params can go here, e.g., { url: 'callback_url' } for status updates ); // Plivo returns message_uuid in an array const messageUuid = response.messageUuid && response.messageUuid.length > 0 ? response.messageUuid[0] : 'N/A'; logger.info(`SMS sent successfully to ${toPhoneNumber}. Message UUID: ${messageUuid}`); return response; } catch (error) { logger.error(`Failed to send SMS to ${toPhoneNumber}. Error: ${error.message}`, { plivoError: error.response ? error.response.data : 'N/A', // Log Plivo specific error if available stack: error.stack, }); // Re-throw the error to be handled by the calling function (scheduler) throw error; } }; module.exports = { sendSmsReminder, };Why a separate service? It isolates Plivo-specific logic, making the code modular and easier to test or swap providers later. We include E.164 validation and throw errors for consistency.
-
Create Reminder Service (
services/reminderService.js): This service contains the logic for finding due appointments and triggering the SMS.javascript// services/reminderService.js const Appointment = require('../models/Appointment'); const plivoService = require('./plivoService'); const logger = require('../config/logger'); const { format } = require('date-fns'); // Using date-fns for reliable date formatting // How many minutes before the appointment to send the reminder const REMINDER_WINDOW_MINUTES = 15; /** * Finds appointments due for reminders and sends SMS via Plivo. */ const checkAndSendReminders = async () => { logger.info('Scheduler checking for appointments needing reminders...'); const now = new Date(); // Calculate the time window [now, now + REMINDER_WINDOW_MINUTES] const reminderCutoffTime = new Date(now.getTime() + REMINDER_WINDOW_MINUTES * 60 * 1000); try { // Find appointments within the window that haven't had a notification sent // Assumes appointment.time is stored in UTC const appointmentsToRemind = await Appointment.find({ time: { $gte: now, // Appointment time is now or in the future $lte: reminderCutoffTime, // Appointment time is within the next N minutes }, notificationSent: false, // Reminder not yet sent }).exec(); // Use exec() for cleaner promise handling if (appointmentsToRemind.length === 0) { logger.info('No appointments found requiring reminders in this cycle.'); return; } logger.info(`Found ${appointmentsToRemind.length} appointments to remind.`); for (const appointment of appointmentsToRemind) { try { // Format time for the message - consider appointment's timezone if stored // For simplicity here, we format based on server time (UTC) - add timezone logic if needed // Ensure appointment.time is a valid Date object before formatting const appointmentDate = new Date(appointment.time); if (isNaN(appointmentDate.getTime())) { logger.error(`Invalid date format for appointment ID ${appointment._id}: ${appointment.time}`); continue; // Skip this appointment } // Example format: ""4:30 PM on December 25, 2024"" const formattedTime = format(appointmentDate, ""h:mm a 'on' MMMM d, yyyy""); const messageBody = `Hi ${appointment.name}, this is a reminder for your appointment scheduled at ${formattedTime}. See you soon!`; await plivoService.sendSmsReminder(appointment.phoneNumber, messageBody); // Mark the appointment as notified IN THE DATABASE appointment.notificationSent = true; await appointment.save(); logger.info(`Successfully processed reminder for appointment ID: ${appointment._id}`); } catch (error) { // Log error for this specific appointment but continue with others logger.error(`Error processing reminder for appointment ID ${appointment._id}: ${error.message}`, { stack: error.stack }); // Optional: Implement retry logic here or flag for manual review // For example, add a field like `notificationFailedAttempts` // appointment.notificationFailedAttempts = (appointment.notificationFailedAttempts || 0) + 1; // await appointment.save(); } } logger.info('Finished processing reminders for this cycle.'); } catch (dbError) { logger.error(`Database error while fetching appointments: ${dbError.message}`, { stack: dbError.stack }); } }; module.exports = { checkAndSendReminders, };Key Logic: It queries MongoDB for appointments where the
timeis between now andnow + REMINDER_WINDOW_MINUTESandnotificationSentis false. It then iterates, sends the SMS viaplivoService, and crucially updatesnotificationSenttotrueto prevent duplicate messages. Error handling is included for both DB queries and individual SMS sends. Correct date formatting string is used. -
Create Scheduler (
services/scheduler.js): This sets up thenode-cronjob.javascript// services/scheduler.js const cron = require('node-cron'); const { checkAndSendReminders } = require('./reminderService'); const logger = require('../config/logger'); let task = null; const startScheduler = () => { // Schedule to run every minute ('*/1 * * * *') // You can adjust the schedule as needed (e.g., '*/5 * * * *' for every 5 minutes) // See https://crontab.guru/ for cron syntax help if (task) { logger.warn('Scheduler already running.'); return; } logger.info('Initializing cron scheduler...'); task = cron.schedule('*/1 * * * *', async () => { logger.info('Cron job triggered: Running checkAndSendReminders...'); try { await checkAndSendReminders(); } catch (error) { // Catch potential top-level errors from checkAndSendReminders itself logger.error(`Unhandled error during cron job execution: ${error.message}`, { stack: error.stack }); } }, { scheduled: true, timezone: ""UTC"" // IMPORTANT: Run scheduler logic in UTC to avoid DST issues // Ensure appointment times are also consistently handled (e.g., stored in UTC) }); logger.info(`Scheduler task started. Will run every minute.`); task.start(); // Explicitly start the task }; const stopScheduler = () => { if (task) { task.stop(); task = null; logger.info('Scheduler task stopped.'); } else { logger.warn('Scheduler task was not running.'); } }; module.exports = startScheduler; // Export the start function // Optionally export stopScheduler if needed elsewhere: // module.exports = { startScheduler, stopScheduler };Why
node-cron? It's simple and integrates directly into the Node application. The schedule'*/1 * * * *'means ""run every minute"". Setting the timezone to UTC is crucial for consistency.
3. Building a Complete API Layer
We need an endpoint to create appointments.
-
Create Appointment Model (
models/Appointment.js):javascript// models/Appointment.js const mongoose = require('mongoose'); const AppointmentSchema = new mongoose.Schema({ name: { type: String, required: [true, 'Customer name is required.'], trim: true, }, phoneNumber: { type: String, required: [true, 'Phone number is required.'], trim: true, // Basic validation for E.164 format - more strict validation can be added match: [/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format (e.g., +14155551212).'], }, time: { type: Date, required: [true, 'Appointment time is required.'], index: true, // Index for faster querying }, timeZone: { // Store the original timezone for potential display purposes type: String, required: [true, 'Timezone is required (e.g., America/New_York).'], trim: true, }, notificationSent: { type: Boolean, default: false, index: true, // Index for faster querying by the scheduler }, // Optional: Track reminder failures // notificationFailedAttempts: { // type: Number, // default: 0 // }, createdAt: { type: Date, default: Date.now, }, }); // Ensure indexes are created AppointmentSchema.index({ time: 1, notificationSent: 1 }); module.exports = mongoose.model('Appointment', AppointmentSchema);Schema Design: Includes necessary fields, basic validation, default values, and crucially, indexes on
timeandnotificationSentfor efficient querying by the scheduler. StoringtimeZoneis good practice, even if thetimeitself is stored in UTC. -
Create Database Connection (
config/db.js):javascript// config/db.js const mongoose = require('mongoose'); const logger = require('./logger'); const connectDB = async () => { try { // Starting from Mongoose v6, options are no longer needed for basic connections const conn = await mongoose.connect(process.env.MONGO_URI); logger.info(`MongoDB Connected: ${conn.connection.host}`); } catch (err) { logger.error(`MongoDB Connection Error: ${err.message}`, { stack: err.stack }); // Exit process with failure process.exit(1); } }; module.exports = connectDB; -
Create Appointment Controller (
controllers/appointmentController.js):javascript// controllers/appointmentController.js const Appointment = require('../models/Appointment'); const logger = require('../config/logger'); const { validationResult } = require('express-validator'); const { zonedTimeToUtc } = require('date-fns-tz'); // For handling timezones correctly /** * @desc Create a new appointment * @route POST /api/appointments * @access Public (Add auth middleware later if needed) */ exports.createAppointment = async (req, res, next) => { // 1. Validation const errors = validationResult(req); if (!errors.isEmpty()) { logger.warn('Validation failed for createAppointment', { errors: errors.array() }); return res.status(400).json({ errors: errors.array() }); } const { name, phoneNumber, time, timeZone } = req.body; try { // 2. Convert scheduled time to UTC for storage // We expect 'time' to be a string like '2025-12-25T14:30:00' // We use the provided timeZone to interpret this local time correctly let appointmentTimeUTC; try { // Ensure the input time string is treated as local time in the specified zone appointmentTimeUTC = zonedTimeToUtc(time, timeZone); } catch (tzError) { logger.warn(`Invalid time or timezone provided: ${time}, ${timeZone}`, { error: tzError.message }); return res.status(400).json({ errors: [{ msg: 'Invalid time or timezone format. Use ISO 8601 format for time (YYYY-MM-DDTHH:mm:ss) and IANA timezone names (e.g., America/New_York).' }] }); } // Ensure the appointment time is in the future if (appointmentTimeUTC <= new Date()) { logger.warn(`Attempt to schedule appointment in the past: ${appointmentTimeUTC}`); return res.status(400).json({ errors: [{ msg: 'Appointment time must be in the future.' }] }); } // 3. Create new appointment instance const newAppointment = new Appointment({ name, phoneNumber, // Already validated for E.164 in model/validator time: appointmentTimeUTC, // Store UTC time timeZone, // Store original timezone }); // 4. Save to database const savedAppointment = await newAppointment.save(); logger.info(`Appointment created successfully: ${savedAppointment._id}`, { name, time: savedAppointment.time }); res.status(201).json({ success: true, message: 'Appointment created successfully.', data: savedAppointment, }); } catch (error) { // Pass error to the global error handler logger.error(`Error creating appointment: ${error.message}`, { stack: error.stack }); next(error); // Pass to centralized error handler } }; /** * @desc Get all appointments (Optional - for testing/admin) * @route GET /api/appointments * @access Public (Add auth middleware later if needed) */ exports.getAppointments = async (req, res, next) => { try { const appointments = await Appointment.find().sort({ time: 1 }); // Sort by time res.status(200).json({ success: true, count: appointments.length, data: appointments }); } catch (error) { logger.error(`Error fetching appointments: ${error.message}`, { stack: error.stack }); next(error); } };Important: This controller handles validation results, converts the incoming local time + timezone into UTC using
date-fns-tzbefore saving, and ensures appointments are scheduled for the future. -
Create Appointment Routes (
routes/appointments.js):javascript// routes/appointments.js const express = require('express'); const { body } = require('express-validator'); const { createAppointment, getAppointments } = require('../controllers/appointmentController'); const validateTimeZone = require('../utils/validateTimeZone'); // Custom validator const router = express.Router(); // Validation middleware for creating an appointment const createAppointmentValidation = [ body('name', 'Name is required').not().isEmpty().trim().escape(), body('phoneNumber', 'Valid E.164 phone number is required (e.g., +14155551212)').matches(/^\+[1-9]\d{1,14}$/), // Use isISO8601({ strict: true, strictSeparator: true }) for stricter format YYYY-MM-DDTHH:mm:ss body('time', 'Appointment time is required in ISO 8601 format (e.g., YYYY-MM-DDTHH:mm:ss)').isISO8601({ strict: true, strictSeparator: true }).withMessage('Use format YYYY-MM-DDTHH:mm:ss for time'), // Use custom validator for timezone body('timeZone').custom(validateTimeZone), ]; // POST /api/appointments - Create a new appointment router.post('/', createAppointmentValidation, createAppointment); // GET /api/appointments - Get all appointments (optional) router.get('/', getAppointments); module.exports = router;Validation: Uses
express-validatorto check incoming request bodies for required fields and correct formats before the data reaches the controller. Includes stricter ISO8601 check and a custom timezone validator. -
Create Timezone Validator (
utils/validateTimeZone.js): We need a way to check if the provided timezone string is valid.javascript// utils/validateTimeZone.js const { DateTime } = require('luxon'); // Luxon is good for timezone validation const validateTimeZone = (value) => { if (!value) { throw new Error('Timezone is required.'); } // Check if the timezone identifier is valid according to Luxon/IANA db if (!DateTime.local().setZone(value).isValid) { throw new Error(`Invalid IANA timezone identifier: '${value}'. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones`); } // If valid, return true for express-validator return true; }; module.exports = validateTimeZone;Why Luxon? Luxon has a robust way (
setZone().isValid) to check if an IANA timezone string is recognized. -
Test API Endpoint: Use
curlor Postman to test thePOST /api/appointmentsendpoint.curlExample:bash# Note: Ensure time format matches YYYY-MM-DDTHH:mm:ss exactly curl -X POST http://localhost:5000/api/appointments \ -H ""Content-Type: application/json"" \ -d '{ ""name"": ""Alice Test"", ""phoneNumber"": ""+14155551212"", ""time"": ""2025-12-24T10:00:00"", ""timeZone"": ""America/Los_Angeles"" }'Expected JSON Response (Success - 201 Created):
json{ ""success"": true, ""message"": ""Appointment created successfully."", ""data"": { ""name"": ""Alice Test"", ""phoneNumber"": ""+14155551212"", ""time"": ""2025-12-24T18:00:00.000Z"", ""timeZone"": ""America/Los_Angeles"", ""notificationSent"": false, ""_id"": ""someGeneratedMongoId"", ""createdAt"": ""2025-04-20T..."", ""__v"": 0 } }Expected JSON Response (Validation Error - 400 Bad Request):
json{ ""errors"": [ { ""type"": ""field"", ""value"": ""2025-12-24 10:00"", ""msg"": ""Use format YYYY-MM-DDTHH:mm:ss for time"", ""path"": ""time"", ""location"": ""body"" } ] }
4. Integrating with Plivo (Review)
We've already set up the Plivo integration within services/plivoService.js. Key aspects:
- Configuration:
PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN, andPLIVO_PHONE_NUMBERare securely loaded from.env. - SDK Initialization: The Plivo client is initialized once when the service module loads.
- Sending SMS: The
sendSmsReminderfunction handles the API call (client.messages.create) with necessary parameters (src,dst,text). Note: Plivo SDK usessrcfor sender anddstfor recipient. - Secrets Handling: API credentials are kept out of the codebase using environment variables via
dotenv.
Obtaining Plivo Credentials:
- Log in to your Plivo Console (https://console.plivo.com/).
- On the main Dashboard (Overview page), you will find your Auth ID and Auth Token. Click the ""Show"" icon if needed.
PLIVO_AUTH_ID: Copy the value labelled ""Auth ID"".PLIVO_AUTH_TOKEN: Copy the value labelled ""Auth Token"".
- Navigate to Phone Numbers -> Your Numbers.
PLIVO_PHONE_NUMBER: Copy one of your Plivo numbers that is SMS-enabled. Ensure it's in the E.164 format (e.g.,+14155551212). If you don't have one, go to Phone Numbers -> Buy Numbers.
5. Implementing Error Handling, Logging, and Retries
- Error Handling Strategy:
- Validation errors are caught early by
express-validatorin routes and returned as 400 responses. - Controller errors (DB issues, unexpected problems) are caught in
try...catchblocks and passed to a centralized error handler usingnext(error). - Service-level errors (e.g., Plivo API failure in
plivoService, DB errors inreminderService) are logged within the service and re-thrown to be handled by the caller (e.g., the scheduler or controller). - Unhandled promise rejections and uncaught exceptions are caught globally in
server.jsand logged using Winston.
- Validation errors are caught early by
- Logging:
- Winston is configured (
config/logger.js) to log to the console and separate files (app.log,error.log,exceptions.log,rejections.log). - Logs include timestamps, levels, messages, metadata, and stack traces for errors.
- Key events like server start, scheduler activity, appointment creation, SMS sending attempts, and errors are logged.
- Winston is configured (
- Retries (Conceptual):
- Database Operations: Mongoose connection logic includes basic retry/exit on initial failure. For specific operations, libraries like
async-retrycould be used within controllers/services if needed. - Plivo SMS Sending: The current
plivoServicelogs errors but doesn't automatically retry. For higher reliability:- Modify
plivoServiceorreminderServiceto catch Plivo errors specifically. - Implement a retry mechanism (e.g., using
async-retryor a simple loop with delays) for transient network errors or specific Plivo error codes. - Consider adding a field like
notificationFailedAttemptsto theAppointmentmodel. Increment this on failure. - Update the scheduler query to potentially retry appointments with
notificationFailedAttempts < MAX_RETRIES. - After max retries, flag the appointment for manual review or stop retrying.
- Modify
- Scheduler Job:
node-cronitself doesn't handle job failures. Thetry...catchblock within the scheduled task inservices/scheduler.jsensures that an error during one run doesn't crash the entire scheduler process.
- Database Operations: Mongoose connection logic includes basic retry/exit on initial failure. For specific operations, libraries like
(Further sections like Deployment, Security, Testing, and Conclusion would typically follow here but were not included in the provided input text.)
Frequently Asked Questions
How to schedule SMS reminders with Node.js?
This involves building an Express.js application with API endpoints to create appointments, which are stored in MongoDB. A background scheduler uses node-cron to check for upcoming appointments and triggers the Plivo SMS API to send reminders based on the stored data.
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 project leverages Plivo's Node.js SDK to integrate SMS functionality seamlessly.
Why use MongoDB for appointment reminders?
MongoDB's flexibility makes it suitable for storing appointment details, including name, phone number, time, and timezone. Mongoose simplifies database interactions within the Node.js application.
When should I send SMS appointment reminders?
The tutorial sets up reminders to be sent 15 minutes before the appointment time. This time window is configurable within the application's reminder service.
How to set up node-cron for scheduling?
Node-cron is used to schedule a task that runs every minute to check for upcoming appointments. The cron expression '*/1 * * * *' defines this schedule, and the timezone is set to UTC for consistency.
What is the purpose of express-validator?
Express-validator is middleware used for validating incoming requests to the API. It checks the request body for required fields (name, phone, time, timezone) and ensures data is in the correct format before it reaches the controller logic.
How to handle timezones with appointment reminders?
The application uses date-fns-tz and Luxon to handle timezones. The user provides their local timezone when creating an appointment, which the app converts to UTC for consistent storage and scheduling, preventing issues with daylight saving time.
What Node.js version is recommended?
Node.js version 16 or later is recommended for this project. This ensures compatibility with the latest features and dependencies used in the tutorial.
How to structure a Node.js Express project?
The tutorial recommends creating directories for config, controllers, models, routes, services, utils, logs, and tests, promoting organized code and separation of concerns. This structure helps maintainability as your project grows.
What is the role of Winston in the application?
Winston provides robust logging capabilities, essential for monitoring in a production environment. It logs to both the console and separate files, recording different log levels, messages, metadata, and error stack traces.
Can I customize the reminder message content?
Yes, the message content can be customized within the reminder service logic. The provided example includes the appointment time and customer name, but you can modify this template to suit your needs.
How to handle Plivo API errors?
The Plivo service logs errors when sending SMS messages fails and rethrows them to be handled by the caller. For production, add retry logic using techniques like async-retry or a retry counter in the appointment model.
How to secure Plivo API credentials?
API credentials (Auth ID, Auth Token) are stored as environment variables in a .env file, which should never be committed to version control. The dotenv package loads these variables into the application's environment.
What is the project setup process?
The process includes creating a project directory, initializing npm, installing required dependencies, configuring nodemon (optional), creating an .env file for credentials, and structuring project files into appropriate folders.
How to test the appointment reminder API?
The tutorial suggests testing the POST /api/appointments endpoint with tools like curl or Postman. Example curl commands and expected JSON responses for success and validation errors are provided.