code examples
code examples
Building a Node.js Express Scheduling & Reminder System with Infobip
A step-by-step guide to creating a production-ready appointment reminder system using Node.js, Express, and the Infobip SMS API.
This guide provides a step-by-step walkthrough for building a production-ready appointment reminder system using Node.js, Express, and the Infobip SMS API. We'll cover everything from project setup and core scheduling logic to robust error handling, security considerations, and deployment.
By the end of this tutorial, you will have a functional application capable of accepting reminder requests via an API, scheduling them, and reliably sending SMS notifications at the specified time using Infobip. This solves the common business need for automated, time-sensitive communication like appointment confirmations, payment due dates, or event reminders.
Last Updated: October 26, 2023
Project Overview and Goals
Goal: To create a backend service that can:
- Receive requests to schedule an SMS reminder via a REST API endpoint.
- Store reminder details (phone number, message, scheduled time).
- Reliably check for due reminders at regular intervals.
- Send the scheduled SMS messages using the Infobip API when they become due.
- Provide basic mechanisms for logging, error handling, and security.
Problem Solved: Automating time-sensitive SMS notifications, reducing manual effort, and improving user engagement or adherence (e.g., reducing missed appointments).
Technologies Used:
- Node.js: Asynchronous JavaScript runtime for building the backend server.
- Express: Minimalist web framework for Node.js, used to create the REST API.
- Infobip Node.js SDK: Simplifies interaction with the Infobip SMS API.
node-cron: A simple cron-like job scheduler for Node.js, used to trigger checks for due reminders.dotenv: Loads environment variables from a.envfile.uuid: Generates unique IDs for reminders.express-validator: Middleware for request validation.- (Optional but Recommended) SQLite: A simple file-based SQL database for persisting reminders (can be swapped for other databases).
System Architecture:
Placeholder: Insert a system architecture diagram image (e.g., PNG or SVG) here. The diagram should illustrate the flow from Client App -> Node.js/Express API -> Reminder Service -> Database, with the Reminder Service interacting with node-cron and the Infobip Service, which in turn calls the Infobip API.
Prerequisites:
- Node.js and npm (or yarn) installed. Install Node.js
- An active Infobip account. Sign up for Infobip
- Basic understanding of JavaScript, Node.js, REST APIs, and asynchronous programming.
- A text editor or IDE (e.g., VS Code).
- A tool for testing APIs (e.g., Postman or
curl).
Final Outcome: A running Node.js application with an API endpoint to schedule SMS reminders, which are then automatically sent at the correct time via Infobip.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
bashmkdir node-infobip-scheduler cd node-infobip-scheduler -
Initialize npm: Initialize the project using npm. The
-yflag accepts default settings.bashnpm init -yThis creates a
package.jsonfile. -
Enable ES Modules: Since the code examples use
import/exportsyntax (ES Modules), add the following line to yourpackage.jsonfile:json// package.json { // ... other settings "type": "module", // ... rest of the file } -
Install Dependencies: Install Express, the Infobip SDK,
node-cron,dotenv,uuid, andexpress-validator.bashnpm install express @infobip-api/sdk node-cron dotenv uuid express-validator -
Install Development Dependencies (Optional but Recommended): Install
nodemonfor automatic server restarts during development.bashnpm install --save-dev nodemon -
(Optional) Install Database Driver: If using SQLite for persistence (recommended over in-memory for reliability):
bashnpm install sqlite3 sqlite # sqlite provides async/await wrapper -
Create Project Structure: Set up a basic folder structure for better organization:
plaintextnode-infobip-scheduler/ ├── src/ │ ├── config/ │ │ └── infobip.js # Infobip SDK configuration │ ├── controllers/ │ │ └── reminderController.js # API request handlers │ ├── routes/ │ │ └── reminderRoutes.js # API routes definition │ ├── services/ │ │ ├── reminderService.js # Core logic for managing reminders │ │ └── schedulerService.js # Logic for cron job scheduling │ ├── middleware/ │ │ └── validator.js # Request validation rules │ └── db/ # Database related files (optional) │ ├── database.js # DB connection/setup │ └── schema.sql # DB schema definition ├── app.js # Express application setup ├── .env # Environment variables (DO NOT COMMIT) ├── .gitignore # Specifies files git should ignore ├── package.json └── package-lock.json -
Create
.gitignore: Create a.gitignorefile in the root directory to prevent sensitive files and unnecessary directories from being committed to version control.plaintext# .gitignore node_modules/ .env *.log db/*.sqlite # Or your specific DB file extension -
Create
.envFile: Create a.envfile in the root directory. This file will hold your Infobip credentials and other configuration. Remember to add.envto your.gitignorefile.ini# .env PORT=3000 INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL # Find this in your Infobip dashboard/API docs # Optional: Database path if using SQLite DATABASE_PATH=./db/reminders.sqlitePORT: The port your Express server will listen on.INFOBIP_API_KEY: Your secret API key from Infobip.INFOBIP_BASE_URL: Your unique base URL provided by Infobip (check your dashboard).DATABASE_PATH: Path to the SQLite database file.
How to find Infobip API Key and Base URL:
- Log in to your Infobip Portal.
- Your Base URL is usually visible on the homepage dashboard after login or in the API documentation section. It's specific to your account/region.
- Navigate to Apps > API Keys (or similar section).
- Click the button to create a new API key (e.g., "New API Key", "Create API Key", or similar text) or use an existing one.
- Copy the generated API Key and your Base URL into your
.envfile.
-
Add npm Scripts: Update the
scriptssection in yourpackage.jsonfor easier starting and development:json// package.json { // ... other settings "type": "module", // Ensure this is present "scripts": { "start": "node src/app.js", "dev": "nodemon src/app.js", "test": "echo \"Error: no test specified\" && exit 1" // Placeholder for tests }, // ... rest of the file }npm start: Runs the application using Node.npm run dev: Runs the application usingnodemonfor auto-restarts.
2. Implementing Core Functionality
Now, let's implement the core logic: configuring Infobip, managing reminders, and setting up the scheduler.
-
Configure Infobip SDK (
src/config/infobip.js): Create a file to initialize the Infobip client instance.javascript// src/config/infobip.js import { Infobip, AuthType } from '@infobip-api/sdk'; import dotenv from 'dotenv'; dotenv.config(); // Load environment variables if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) { console.error(""FATAL ERROR: Infobip API Key or Base URL not configured in .env file.""); process.exit(1); // Exit if essential config is missing } const infobipClient = new Infobip({ baseUrl: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, authType: AuthType.ApiKey, }); export default infobipClient;- We import the necessary parts from the SDK and
dotenv. - We explicitly check for the presence of API Key and Base URL, exiting if they are missing to prevent runtime errors.
- We create and export a configured
infobipClientinstance.
- We import the necessary parts from the SDK and
-
Setup Database (Optional - SQLite Example) (
src/db/database.jsandsrc/db/schema.sql): If using SQLite, set up the database connection and ensure the table exists.-
Schema Definition (
src/db/schema.sql):sql-- src/db/schema.sql CREATE TABLE IF NOT EXISTS reminders ( id TEXT PRIMARY KEY, phoneNumber TEXT NOT NULL, message TEXT NOT NULL, scheduleTime TEXT NOT NULL, -- Store as ISO 8601 string (UTC) status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'sent', 'failed' createdAt TEXT DEFAULT CURRENT_TIMESTAMP, updatedAt TEXT DEFAULT CURRENT_TIMESTAMP ); -- Optional: Trigger to update 'updatedAt' timestamp automatically CREATE TRIGGER IF NOT EXISTS update_reminders_updatedAt AFTER UPDATE ON reminders FOR EACH ROW BEGIN UPDATE reminders SET updatedAt = CURRENT_TIMESTAMP WHERE id = OLD.id; END; -- Optional: Index for faster querying of pending reminders by time CREATE INDEX IF NOT EXISTS idx_reminders_pending_scheduleTime ON reminders (status, scheduleTime) WHERE status = 'pending'; -
Database Setup (
src/db/database.js): (Note: This code uses the ES Module pattern to get the directory path, replacing the CommonJS__dirname)javascript// src/db/database.js import sqlite3 from 'sqlite3'; import { open } from 'sqlite'; import fs from 'fs'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; import dotenv from 'dotenv'; // ES Module equivalent for __dirname const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); dotenv.config(); // Use path.resolve to ensure the path is absolute or relative to the project root as intended const defaultDbPath = path.resolve(__dirname, 'reminders.sqlite'); const dbPath = process.env.DATABASE_PATH || defaultDbPath; const dbDir = path.dirname(dbPath); // Ensure the database directory exists if (!fs.existsSync(dbDir)) { try { fs.mkdirSync(dbDir, { recursive: true }); console.log(`Database directory created at: ${dbDir}`); } catch (error) { console.error(`Error creating database directory at ${dbDir}:`, error); process.exit(1); } } let db; async function initializeDatabase() { if (db) return db; // Return existing connection if already initialized try { db = await open({ filename: dbPath, driver: sqlite3.Database }); console.log('Connected to the SQLite database.'); // Read and execute schema SQL const schemaPath = path.resolve(__dirname, 'schema.sql'); const schemaSql = fs.readFileSync(schemaPath, 'utf8'); await db.exec(schemaSql); console.log('Database schema ensured.'); return db; } catch (error) { console.error('Error initializing SQLite database:', error); process.exit(1); // Exit if database cannot be initialized } } // Function to get the database instance async function getDb() { if (!db) { await initializeDatabase(); } return db; } export { getDb, initializeDatabase }; // Export initializeDatabase if needed elsewhere- Uses the
import.meta.urlpattern to correctly determine the directory path in ES Modules. - Sets up a connection using the
sqlitelibrary (which provides async/await support oversqlite3). - Reads the
schema.sqlfile and executes it to ensure thereminderstable and indexes exist on startup. - Handles potential errors during initialization.
- Uses the
-
-
Reminder Service (
src/services/reminderService.js): This service handles the business logic for creating, retrieving, and updating reminders.javascript// src/services/reminderService.js import { v4 as uuidv4 } from 'uuid'; // Choose ONE implementation: Database or In-Memory // Option 1: Database (SQLite) - Recommended for persistence import { getDb } from '../db/database.js'; // Option 2: In-Memory (Simpler, data lost on restart) - Comment out DB import if using this // let reminders = []; // In-memory store import infobipClient from '../config/infobip.js'; // --- Database Implementation (SQLite) --- // Make sure the 'getDb' import above is active if using this section. // Comment out this entire section if using the In-Memory alternative below. async function addReminder(phoneNumber, message, scheduleTime) { const db = await getDb(); const id = uuidv4(); const isoScheduleTime = new Date(scheduleTime).toISOString(); // Ensure UTC ISO format // Basic validation (more robust validation in controller/middleware) if (isNaN(new Date(isoScheduleTime).getTime())) { throw new Error('Invalid scheduleTime format. Please use ISO 8601 format.'); } if (new Date(isoScheduleTime) <= new Date()) { throw new Error('Schedule time must be in the future.'); } try { const result = await db.run( 'INSERT INTO reminders (id, phoneNumber, message, scheduleTime) VALUES (?, ?, ?, ?)', [id, phoneNumber, message, isoScheduleTime] ); console.log(`Reminder added with ID: ${id}`); return { id, phoneNumber, message, scheduleTime: isoScheduleTime, status: 'pending' }; } catch (error) { console.error('Error adding reminder to DB:', error); throw new Error('Failed to schedule reminder.'); // Propagate generic error } } async function getDueReminders() { const db = await getDb(); const now = new Date().toISOString(); try { // Select pending reminders where scheduleTime is now or in the past const reminders = await db.all( ""SELECT * FROM reminders WHERE status = 'pending' AND scheduleTime <= ?"", [now] ); return reminders; } catch (error) { console.error('Error fetching due reminders:', error); return []; // Return empty array on error to prevent scheduler crash } } async function updateReminderStatus(id, status, errorInfo = null) { const db = await getDb(); try { // In a real app, you might store more detailed error info const updateSql = status === 'failed' ? ""UPDATE reminders SET status = ?, updatedAt = CURRENT_TIMESTAMP WHERE id = ?"" // Add error logging/storage here if needed : ""UPDATE reminders SET status = ?, updatedAt = CURRENT_TIMESTAMP WHERE id = ?""; await db.run(updateSql, [status, id]); console.log(`Reminder ${id} status updated to: ${status}`); if (status === 'failed' && errorInfo) { console.error(`Reminder ${id} failed:`, errorInfo); } } catch (error) { console.error(`Error updating reminder ${id} status to ${status}:`, error); // Decide if this error is critical enough to stop the scheduler or just log } } // --- End Database Implementation --- /* // --- In-Memory Implementation (Alternative - Simpler, but not persistent) --- // Uncomment this section and comment out the Database Implementation above if using this. // Ensure the 'getDb' import at the top is commented out. let reminders = []; // In-memory store async function addReminder(phoneNumber, message, scheduleTime) { const id = uuidv4(); const isoScheduleTime = new Date(scheduleTime).toISOString(); if (isNaN(new Date(isoScheduleTime).getTime())) { throw new Error('Invalid scheduleTime format. Please use ISO 8601 format.'); } if (new Date(isoScheduleTime) <= new Date()) { throw new Error('Schedule time must be in the future.'); } const newReminder = { id, phoneNumber, message, scheduleTime: isoScheduleTime, status: 'pending', createdAt: new Date().toISOString() }; reminders.push(newReminder); console.log(`Reminder added with ID: ${id}`); return newReminder; } async function getDueReminders() { const now = new Date(); return reminders.filter(r => r.status === 'pending' && new Date(r.scheduleTime) <= now); } async function updateReminderStatus(id, status, errorInfo = null) { const index = reminders.findIndex(r => r.id === id); if (index !== -1) { reminders[index].status = status; reminders[index].updatedAt = new Date().toISOString(); console.log(`Reminder ${id} status updated to: ${status}`); if (status === 'failed' && errorInfo) { console.error(`Reminder ${id} failed:`, errorInfo); } } else { console.warn(`Attempted to update status for non-existent reminder ID: ${id}`); } } // --- End In-Memory Implementation --- */ // --- Infobip Sending Logic (Used by both implementations) --- async function sendSms(phoneNumber, message) { console.log(`Attempting to send SMS to ${phoneNumber}`); try { const response = await infobipClient.channels.sms.send({ messages: [ { destinations: [{ to: phoneNumber }], from: 'InfoSMS', // Or your registered Sender ID / purchased number text: message, }, ], }); // Log success and potentially the message ID const sentMessageInfo = response.data.messages[0]; console.log(`SMS sent successfully to ${phoneNumber}. Message ID: ${sentMessageInfo?.messageId}, Status: ${sentMessageInfo?.status?.name}`); return { success: true, response: response.data }; } catch (error) { console.error(`Error sending SMS via Infobip to ${phoneNumber}:`, error.response?.data || error.message); // Extract meaningful error details if available from Infobip's response const errorDetails = error.response?.data?.requestError?.serviceException || { message: error.message }; // Ensure errorDetails is an object or string return { success: false, error: errorDetails }; } } // Export the functions relevant to the chosen implementation (DB or In-Memory) export { addReminder, getDueReminders, updateReminderStatus, sendSms };- Provides functions to interact with the data store (either SQLite or in-memory). Includes clear comments guiding the user to choose ONE implementation.
- Includes the
sendSmsfunction using the configuredinfobipClient. - Handles basic validation for schedule time.
- Includes basic error logging for database and Infobip operations.
- Crucially, it stores and compares dates in UTC (using
toISOString()).
-
Scheduler Service (
src/services/schedulerService.js): This service usesnode-cronto periodically check for and process due reminders.javascript// src/services/schedulerService.js import cron from 'node-cron'; import { getDueReminders, updateReminderStatus, sendSms } from './reminderService.js'; // Schedule a task to run every minute. Adjust the cron pattern as needed. // Pattern: second(opt) minute hour day(month) month day(week) // '* * * * *' = runs every minute const cronJob = cron.schedule('* * * * *', async () => { console.log(`[${new Date().toISOString()}] Running scheduler: Checking for due reminders...`); try { const dueReminders = await getDueReminders(); if (dueReminders.length === 0) { // console.log(`[${new Date().toISOString()}] No due reminders found.`); // Optional: reduce noise return; } console.log(`[${new Date().toISOString()}] Found ${dueReminders.length} due reminders. Processing...`); // Process reminders sequentially to avoid overwhelming downstream systems or rate limits for (const reminder of dueReminders) { console.log(`Processing reminder ID: ${reminder.id} for ${reminder.phoneNumber}`); const sendResult = await sendSms(reminder.phoneNumber, reminder.message); if (sendResult.success) { await updateReminderStatus(reminder.id, 'sent'); } else { // Basic retry could be added here, or just mark as failed const errorMessage = sendResult.error?.message || JSON.stringify(sendResult.error); console.error(`Failed to send reminder ID: ${reminder.id}. Error: ${errorMessage}`); await updateReminderStatus(reminder.id, 'failed', errorMessage); // Consider implementing a more robust retry mechanism (e.g., exponential backoff) // or notifying an admin system about the failure. } // Optional: Add a small delay between sends if needed to respect rate limits // await new Promise(resolve => setTimeout(resolve, 100)); } console.log(`[${new Date().toISOString()}] Finished processing batch of ${dueReminders.length} reminders.`); } catch (error) { // Catch errors during the fetching or processing loop console.error(`[${new Date().toISOString()}] CRITICAL ERROR in scheduler run:`, error); // Potentially add alerting here if the scheduler itself fails critically } }, { scheduled: false // Don't start automatically; we'll start it in app.js }); function startScheduler() { console.log('Starting scheduler service...'); cronJob.start(); } function stopScheduler() { console.log('Stopping scheduler service...'); cronJob.stop(); } export { startScheduler, stopScheduler };- Imports
node-cronand the necessary functions fromreminderService. - Defines a cron job (
cronJob) that runs every minute (* * * * *). You can adjust this frequency based on your needs (e.g.,*/5 * * * *for every 5 minutes). - The job fetches due reminders, iterates through them, attempts to send the SMS, and updates the status accordingly ('sent' or 'failed').
- Includes basic logging for scheduler activity and errors.
- Exports
startSchedulerandstopSchedulerfunctions to control the job lifecycle.
- Imports
3. Building the API Layer
We'll now create the Express API endpoint to accept requests for scheduling reminders.
-
Request Validation Middleware (
src/middleware/validator.js): Define validation rules usingexpress-validator.javascript// src/middleware/validator.js import { body, validationResult } from 'express-validator'; const validateReminderRequest = [ // Validate phone number: basic check for E.164 format. // NOTE: This regex is a simplified check. For robust validation, // consider using a library like 'libphonenumber-js'. body('phoneNumber') .trim() .notEmpty().withMessage('Phone number is required.') .matches(/^\+?[1-9]\d{1,14}$/).withMessage('Invalid phone number format. Use E.164 format (e.g., +14155552671).'), // Validate message: ensure it's not empty and within reasonable length body('message') .trim() .notEmpty().withMessage('Message is required.') .isLength({ min: 1, max: 1600 }).withMessage('Message length must be between 1 and 1600 characters.'), // Max SMS length can vary // Validate scheduleTime: ensure it's a valid ISO 8601 date string body('scheduleTime') .notEmpty().withMessage('Schedule time is required.') .isISO8601({ strict: true, require_tld: false }).withMessage('Schedule time must be a valid ISO 8601 date string (e.g., 2024-12-31T10:00:00Z or 2024-12-31T10:00:00+01:00).') .custom((value) => { // Custom validation to ensure the date is in the future if (new Date(value) <= new Date()) { throw new Error('Schedule time must be in the future.'); } return true; }), // Middleware function to handle validation results (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { // Format errors for a cleaner response const formattedErrors = errors.array().map(err => ({ field: err.param, // Use 'param' instead of 'path' for body validation message: err.msg, value: err.value, })); return res.status(400).json({ errors: formattedErrors }); } next(); // Proceed if validation passes } ]; export { validateReminderRequest };- Uses
body()to specify rules forphoneNumber,message, andscheduleTime. - Includes a note about the simplified phone number regex.
- Includes checks for emptiness, format (E.164 for phone, ISO 8601 for date), length, and ensures the date is in the future.
- The final middleware function checks
validationResultand returns a 400 error with details if validation fails.
- Uses
-
Reminder Controller (
src/controllers/reminderController.js): Handle the logic for the API endpoint.javascript// src/controllers/reminderController.js import { addReminder } from '../services/reminderService.js'; const scheduleReminder = async (req, res, next) => { // Added next for error handling // Validation has already passed via middleware const { phoneNumber, message, scheduleTime } = req.body; try { const newReminder = await addReminder(phoneNumber, message, scheduleTime); // Respond with the created reminder details (excluding sensitive info if any) res.status(201).json({ message: 'Reminder scheduled successfully.', reminder: { id: newReminder.id, phoneNumber: newReminder.phoneNumber, // Consider masking if needed message: newReminder.message, scheduleTime: newReminder.scheduleTime, status: newReminder.status, } }); } catch (error) { // Handle errors from the service layer (e.g., DB error, invalid date logic) console.error('Error in scheduleReminder controller:', error); // Pass the error to the global error handler OR send a specific response // Option 1: Pass to global handler next(error); // Option 2: Send specific response (less consistent if you have a global handler) // res.status(500).json({ message: 'Failed to schedule reminder.', error: error.message }); } }; // Add other controllers here if needed (e.g., getReminderStatus, cancelReminder) export { scheduleReminder };- Imports the
addReminderfunction. - Extracts validated data from
req.body. - Calls the service function to create the reminder.
- Responds with a 201 status code and the created reminder's details on success, or passes errors to the global handler (or sends 500).
- Imports the
-
Reminder Routes (
src/routes/reminderRoutes.js): Define the API routes and associate them with controllers and middleware.javascript// src/routes/reminderRoutes.js import express from 'express'; import { scheduleReminder } from '../controllers/reminderController.js'; import { validateReminderRequest } from '../middleware/validator.js'; // Optional: Add authentication middleware here // import { authenticate } from '../middleware/auth.js'; const router = express.Router(); // POST /api/reminders - Schedule a new reminder // Optional: Add authentication middleware: router.post('/', authenticate, validateReminderRequest, scheduleReminder); router.post('/', validateReminderRequest, scheduleReminder); // Add other routes here (GET /:id, DELETE /:id, etc.) if needed export default router;- Creates an Express router instance.
- Defines a
POSTroute at/. - Applies the
validateReminderRequestmiddleware first, then thescheduleRemindercontroller. - Includes a commented-out placeholder for authentication middleware.
-
Express App Setup (
src/app.js): Configure the main Express application, load middleware, routes, and start the server and scheduler.javascript// src/app.js import express from 'express'; import dotenv from 'dotenv'; import reminderRoutes from './routes/reminderRoutes.js'; import { startScheduler, stopScheduler } from './services/schedulerService.js'; // Import stopScheduler import { getDb, initializeDatabase } from './db/database.js'; // Import DB init and getDb // Load environment variables dotenv.config(); 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) => { const start = Date.now(); res.on('finish', () => { // Log when response is finished const duration = Date.now() - start; console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`); }); next(); }); // --- Routes --- app.get('/health', (req, res) => { // Simple health check endpoint res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); app.use('/api/reminders', reminderRoutes); // Mount reminder routes // --- 404 Handler for unmatched routes --- app.use((req, res, next) => { res.status(404).json({ message: 'Not Found' }); }); // --- Global Error Handler --- // Catches errors passed via next(error) app.use((err, req, res, next) => { console.error("Unhandled Error:", err.stack || err); // Avoid sending detailed errors in production const isDevelopment = process.env.NODE_ENV === 'development'; res.status(err.status || 500).json({ message: err.message || 'Internal Server Error', // Optionally include stack trace ONLY in development stack: isDevelopment ? err.stack : undefined, }); }); // --- Start Server and Services --- let server; // Variable to hold the server instance for graceful shutdown async function startServer() { // Database initialization (if using DB) try { // Check if using database by checking if DATABASE_PATH is set or default is used // This logic assumes you only initialize DB if you intend to use it. // Adjust this check based on your actual setup (e.g., an explicit config flag). const usingDb = process.env.DATABASE_PATH || fs.existsSync(path.resolve(dirname(fileURLToPath(import.meta.url)), 'db', 'reminders.sqlite')); if (usingDb) { await initializeDatabase(); // Ensure DB is ready before starting scheduler/server console.log('Database initialized successfully.'); } else { console.log('Running without database persistence (using in-memory store).'); } // Start the scheduler startScheduler(); // Start the Express server server = app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); console.log(`API endpoint available at http://localhost:${PORT}/api/reminders`); console.log(`Health check available at http://localhost:${PORT}/health`); }); // Handle graceful shutdown process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown); } catch (error) { console.error('Failed to start the application:', error); process.exit(1); } } function gracefulShutdown() { console.log('Received shutdown signal. Closing server and scheduler...'); stopScheduler(); // Stop the cron job from scheduling new checks if (server) { server.close(async () => { console.log('HTTP server closed.'); // Optional: Close database connection if applicable try { const db = await getDb(); // Attempt to get DB instance if (db && typeof db.close === 'function') { // Check if db exists and has close method await db.close(); console.log('Database connection closed.'); } } catch (dbError) { console.error('Error closing database connection:', dbError); } finally { console.log('Shutdown complete.'); process.exit(0); // Exit gracefully } }); } else { console.log('No active server to close.'); process.exit(0); } // Force shutdown if server doesn't close within a timeout setTimeout(() => { console.error('Could not close connections in time, forcing shutdown.'); process.exit(1); }, 10000); // 10 seconds timeout } // Start the application startServer();
Frequently Asked Questions
How to schedule SMS reminders with Node.js and Express?
This tutorial provides a comprehensive guide to building an SMS reminder system using Node.js, Express, and the Infobip API. You'll learn how to set up the project, handle API requests, schedule reminders, and send SMS notifications reliably. The system uses node-cron for scheduling and offers options for persistent storage with SQLite or an in-memory store for simplicity.
What is the Infobip SMS API used for in this project?
The Infobip SMS API is the core communication component of the reminder system. It's used to send the actual SMS messages to users at the scheduled time. The Infobip Node.js SDK simplifies interaction with the API, handling authentication and requests.
Why does this project use node-cron?
Node-cron is a task scheduler for Node.js that allows you to define tasks to run at specific intervals, similar to cron jobs. This is essential for checking the database or in-memory store periodically for reminders that are due to be sent.
When should I use SQLite for reminder storage?
SQLite is recommended for production environments or any scenario where data persistence is required. If the server restarts, reminders stored in SQLite will be preserved, unlike the in-memory option where data is lost on restart.
Can I use a different database for reminder storage?
While the tutorial uses SQLite as an example for persistent storage, you can adapt it to other databases. The core logic for interacting with the database is encapsulated in the `reminderService.js` file, allowing for flexibility in database choice.
How to set up the Infobip API key and base URL?
You'll need an active Infobip account. The API key and base URL can be found in your Infobip portal. Create a `.env` file in your project's root directory and store these credentials there. Ensure the `.env` file is added to your `.gitignore` to prevent it from being committed to version control.
What is the purpose of express-validator in this system?
Express-validator is used to validate incoming API requests, ensuring that required fields like phone number, message, and schedule time are present and in the correct format. This adds robustness and security to the system.
How to validate phone numbers effectively in Node.js?
The tutorial provides a basic regex for E.164 phone number format validation. For more comprehensive validation, consider using a specialized library like `libphonenumber-js` which can handle various international phone number formats and validation rules.
What are the prerequisites for building this reminder system?
You should have Node.js and npm (or yarn) installed. An active Infobip account and API key are required. A basic understanding of REST APIs, asynchronous programming, and JavaScript is also recommended.
How to handle errors in the Node.js reminder system?
The project demonstrates basic error handling, including checking for configuration errors, catching database issues, logging Infobip API errors, and using a global error handler in Express to catch and format errors for appropriate responses.
How to test the SMS reminder API?
Once the server is running, you can use tools like Postman or `curl` to send test requests to the API endpoint (`/api/reminders`). These tools allow you to send POST requests with the required data (phone number, message, and schedule time) and examine the responses.
How to run the Node.js reminder application?
After setting up the project, use `npm start` to run the application in production mode. For development, use `npm run dev` which automatically restarts the server whenever you make changes to the code. This requires installing nodemon as a development dependency.
What does the system architecture diagram represent?
The diagram illustrates the flow of data and interactions within the reminder system. It visually represents how client requests flow through the API, interact with the services, and eventually send messages via the Infobip API.
What are the main technologies used in this reminder system?
The project uses Node.js with Express for building the backend API and handling requests. Infobip's SMS API and Node.js SDK handle messaging, while node-cron schedules tasks. SQLite provides optional persistent storage, and express-validator manages request validation.
Why is it important to store schedule times in UTC?
Storing schedule times in UTC (using toISOString()) ensures consistency and avoids issues related to time zones. This is especially important in reminder systems to ensure that messages are sent at the correct time, regardless of the server's location or the user's time zone.