code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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:

  1. Receive requests to schedule an SMS reminder via a REST API endpoint.
  2. Store reminder details (phone number, message, scheduled time).
  3. Reliably check for due reminders at regular intervals.
  4. Send the scheduled SMS messages using the Infobip API when they become due.
  5. 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 .env file.
  • 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.

  1. Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.

    bash
    mkdir node-infobip-scheduler
    cd node-infobip-scheduler
  2. Initialize npm: Initialize the project using npm. The -y flag accepts default settings.

    bash
    npm init -y

    This creates a package.json file.

  3. Enable ES Modules: Since the code examples use import/export syntax (ES Modules), add the following line to your package.json file:

    json
    // package.json
    {
      // ... other settings
      "type": "module",
      // ... rest of the file
    }
  4. Install Dependencies: Install Express, the Infobip SDK, node-cron, dotenv, uuid, and express-validator.

    bash
    npm install express @infobip-api/sdk node-cron dotenv uuid express-validator
  5. Install Development Dependencies (Optional but Recommended): Install nodemon for automatic server restarts during development.

    bash
    npm install --save-dev nodemon
  6. (Optional) Install Database Driver: If using SQLite for persistence (recommended over in-memory for reliability):

    bash
    npm install sqlite3 sqlite # sqlite provides async/await wrapper
  7. Create Project Structure: Set up a basic folder structure for better organization:

    plaintext
    node-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
  8. Create .gitignore: Create a .gitignore file 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
  9. Create .env File: Create a .env file in the root directory. This file will hold your Infobip credentials and other configuration. Remember to add .env to your .gitignore file.

    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.sqlite
    • PORT: 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:

    1. Log in to your Infobip Portal.
    2. 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.
    3. Navigate to Apps > API Keys (or similar section).
    4. 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.
    5. Copy the generated API Key and your Base URL into your .env file.
  10. Add npm Scripts: Update the scripts section in your package.json for 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 using nodemon for auto-restarts.

2. Implementing Core Functionality

Now, let's implement the core logic: configuring Infobip, managing reminders, and setting up the scheduler.

  1. 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 infobipClient instance.
  2. Setup Database (Optional - SQLite Example) (src/db/database.js and src/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.url pattern to correctly determine the directory path in ES Modules.
      • Sets up a connection using the sqlite library (which provides async/await support over sqlite3).
      • Reads the schema.sql file and executes it to ensure the reminders table and indexes exist on startup.
      • Handles potential errors during initialization.
  3. 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 sendSms function using the configured infobipClient.
    • 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()).
  4. Scheduler Service (src/services/schedulerService.js): This service uses node-cron to 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-cron and the necessary functions from reminderService.
    • 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 startScheduler and stopScheduler functions to control the job lifecycle.

3. Building the API Layer

We'll now create the Express API endpoint to accept requests for scheduling reminders.

  1. Request Validation Middleware (src/middleware/validator.js): Define validation rules using express-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 for phoneNumber, message, and scheduleTime.
    • 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 validationResult and returns a 400 error with details if validation fails.
  2. 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 addReminder function.
    • 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).
  3. 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 POST route at /.
    • Applies the validateReminderRequest middleware first, then the scheduleReminder controller.
    • Includes a commented-out placeholder for authentication middleware.
  4. 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.