This guide provides a step-by-step walkthrough for building a production-ready Node.js and Express application that schedules and sends SMS appointment reminders using the Sinch SMS API. We'll cover everything from initial project setup to deployment and monitoring best practices.
While the example focuses on hospital appointment reminders, the principles and techniques can be easily adapted for various scheduling use cases, such as meeting reminders, event notifications, or subscription renewal alerts. By leveraging Sinch's built-in scheduling capabilities, we can create a robust and reliable reminder system without needing complex local scheduling libraries or persistent background processes for the sending mechanism itself.
Prerequisites:
- Node.js and npm (or yarn): Ensure you have Node.js (LTS version recommended) and its package manager installed. Download Node.js
- Sinch Account: You need a Sinch account to obtain API credentials and a sending number. Sign up for Sinch
- Sinch API Credentials:
- Project ID
- API Key ID
- API Key Secret
- A provisioned Sinch phone number (virtual number) capable of sending SMS in your target region(s).
- Verified Recipient Number (for testing): During initial development and testing with a free trial or standard account, Sinch typically requires you to verify the phone numbers you send messages to. You can usually do this within your Sinch Customer Dashboard.
- Basic Familiarity: Understanding of Node.js, Express, JavaScript (including
async
/await
), HTML forms, and REST APIs is helpful.
What You'll Build:
- An Express web application with a simple UI to input appointment details (patient name, doctor name, appointment date/time, patient phone number).
- Backend logic to receive appointment data, validate it, and schedule an SMS reminder using the Sinch SMS API's
send_at
feature. - Secure handling of API credentials using environment variables.
- Basic error handling and logging.
- Guidance on production considerations like database persistence, security enhancements, monitoring, and deployment.
System Architecture:
The application follows a simple web application architecture:
graph LR
A[User Browser] -- HTTP Request (Submit Form) --> B(Node.js/Express App);
B -- Schedule SMS (API Call w/ send_at) --> C(Sinch SMS API);
C -- Scheduled Time --> D(Sends SMS);
D -- SMS --> E(Recipient Phone);
B -- HTTP Response (Success/Error Page) --> A;
B -- Store Appointment (DB Write) --> F[(Database)];
B -- Retrieve Appointment (DB Read) --> F;
- User Browser: Interacts with the Express app's frontend to submit appointment details.
- Node.js/Express App:
- Serves the HTML form.
- Receives form submissions.
- Validates input data.
- Uses the Sinch Node SDK (
@sinch/sdk-core
) to make an API call. - Crucially, it tells the Sinch API when to send the message using the
send_at
parameter. - (Production Requirement): Persists appointment data to a database.
- Renders a success or error page back to the user (potentially fetching confirmation data from the database).
- Sinch SMS API:
- Receives the request to send an SMS.
- Stores the message and the scheduled
send_at
time. - Handles the actual sending of the SMS at the specified future time.
- Recipient Phone: Receives the SMS reminder.
- Database: Persistently stores appointment details, ensuring data survives server restarts and is available for retrieval.
1. Setting Up the Project
Let's initialize the project, install dependencies, and configure the basic structure.
-
Create Project Directory: Open your terminal or command prompt and create a directory for the project.
mkdir sinch-appointment-reminder cd sinch-appointment-reminder
-
Initialize Node.js Project: This creates a
package.json
file to manage project details and dependencies. The-y
flag accepts default settings.npm init -y
-
Create Project Structure: Set up the necessary folders and files.
# Create directories mkdir public public/css views # Create files (use `touch` on macOS/Linux or `echo. >` on Windows) touch .env .gitignore app.js routes.js public/css/style.css views/patient_details.ejs views/success.ejs
.env
: Stores environment variables (API keys, secrets). Never commit this file to Git..gitignore
: Specifies files/folders Git should ignore.app.js
: The main entry point for the Express application.routes.js
: Handles application routing and request logic.public/
: Contains static assets (CSS, client-side JS, images).views/
: Contains server-side templates (EJS files in this case). Note: Changed file extensions to.ejs
.
-
Install Dependencies: Install the necessary npm packages.
npm install express @sinch/sdk-core dotenv luxon express-session connect-flash ejs
express
: The web framework for Node.js.@sinch/sdk-core
: The official Sinch Node.js SDK for interacting with Sinch APIs (including SMS).dotenv
: Loads environment variables from the.env
file intoprocess.env
.luxon
: A powerful library for working with dates and times, crucial for handling appointment times and time zones.express-session
: Middleware for managing user sessions (used here for flash messages and temporary data transfer between requests).connect-flash
: Middleware for displaying temporary messages (flash messages) to the user, often used for success or error feedback after form submissions.ejs
: A simple templating engine to render HTML dynamically.- Note: We are not installing
sessionstorage-for-nodejs
. It is unsuitable for production as its data is lost on server restarts. For passing temporary confirmation data to the success page, we will use the standardexpress-session
mechanism. For persistent storage of appointments, a database is required (see Section 6).
-
Install Development Dependency: Install
nodemon
to automatically restart the server during development when files change.npm install --save-dev nodemon
-
Configure
.gitignore
: Prevent sensitive files and unnecessary folders from being committed to version control.# .gitignore # Dependencies node_modules # Environment variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # OS generated files .DS_Store Thumbs.db
-
Configure Environment Variables (
.env
): Add your Sinch credentials and configuration. Replace the placeholder values with your actual credentials.# .env # Sinch API Credentials & Configuration # Obtain from Sinch Customer Dashboard -> Access Keys -> Create API Key KEY_ID='YOUR_SINCH_KEY_ID' KEY_SECRET='YOUR_SINCH_KEY_SECRET' PROJECT_ID='YOUR_SINCH_PROJECT_ID' # Sinch Phone Number (Must be SMS enabled and assigned to your Project) # Obtain from Sinch Customer Dashboard -> Numbers -> Your Numbers FROM_NUMBER='+1xxxxxxxxxx' # Use E.164 format (e.g., +12025550181) # Sinch SMS API Region (e.g., 'us' or 'eu') # Determines the API endpoint and potentially default country code logic SMS_REGION='us' # Application Configuration PORT=3000 # Generate a strong random string for production! SESSION_SECRET='replace_with_a_strong_random_secret_string' # Optional: Default Country Code (Used for basic phone formatting logic) # Example for US DEFAULT_COUNTRY_CODE='+1' # Example for UK # DEFAULT_COUNTRY_CODE='+44'
KEY_ID
,KEY_SECRET
,PROJECT_ID
: Found on the "Access Keys" page in your Sinch Customer Dashboard. You might need to create a new key. Treat theKEY_SECRET
like a password.FROM_NUMBER
: A virtual number you've acquired through Sinch and assigned to your project, enabled for SMS. Find this under "Numbers". Ensure it's in E.164 format.SMS_REGION
: Specify the Sinch API region (us
,eu
, etc.) you are primarily targeting. This affects API endpoints and potentially compliance.PORT
: The port your Express app will listen on.SESSION_SECRET
: A random, long, and secret string used to sign the session ID cookie. Generate a strong one for production.DEFAULT_COUNTRY_CODE
: Useful if you need to prepend a country code to phone numbers entered without one, based on your primary operating region.
-
Set Up
nodemon
Script: Add a script to yourpackage.json
for easy development server startup.// package.json ("scripts" section) "scripts": { "start": "node app.js", "dev": "nodemon app.js", "test": "echo \"Error: no test specified\" && exit 1" },
Now you can run
npm run dev
to start the server withnodemon
. -
Configure Application Entry Point (
app.js
): Set up the basic Express application, middleware, view engine, and server listener.// app.js const express = require('express'); const session = require('express-session'); const flash = require('connect-flash'); const path = require('path'); const routes = require('./routes'); // Import routes require('dotenv').config(); // Load .env variables early const app = express(); const port = process.env.PORT || 3000; // View engine setup (EJS) app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); // Middleware // Serve static files (CSS, client-side JS) from 'public' directory app.use(express.static(path.join(__dirname, 'public'))); // Parse URL-encoded bodies (as sent by HTML forms) app.use(express.urlencoded({ extended: false })); // Session middleware configuration app.use( session({ secret: process.env.SESSION_SECRET || 'default_fallback_secret', // Use env var resave: false, // Don't save session if unmodified saveUninitialized: false, // Don't create session until something stored cookie: { maxAge: 3600000, // Cookie expires in 1 hour (in milliseconds) // secure: process.env.NODE_ENV === 'production', // Use secure cookies in production (HTTPS) // httpOnly: true // Helps prevent XSS attacks } }) ); // Flash message middleware (depends on session) app.use(flash()); // Global variables for views (e.g., making flash messages available) app.use((req, res, next) => { res.locals.success_msg = req.flash('success_msg'); res.locals.error_msg = req.flash('error_msg'); // Make environment variables available to templates if needed (use with caution) res.locals.process = { env: { DEFAULT_COUNTRY_CODE: process.env.DEFAULT_COUNTRY_CODE } }; next(); }); // Mount the routes defined in routes.js app.use('/', routes); // Basic Error Handler (optional but recommended) app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('Something broke!'); }); // Start the server app.listen(port, () => { console.log(`Server started on http://localhost:${port}`); console.log(`Sinch Project ID: ${process.env.PROJECT_ID}`); console.log(`Sinch SMS Region: ${process.env.SMS_REGION}`); });
- We load
.env
variables usingdotenv.config()
. - Standard middleware (
express.static
,express.urlencoded
) is configured. express-session
andconnect-flash
are set up. TheSESSION_SECRET
is used, and thecookie.maxAge
is set to 1 hour (adjust as needed). Security options likesecure
andhttpOnly
are commented but recommended for production.- A simple middleware makes flash messages available globally in our EJS views (
res.locals
). We also pass theDEFAULT_COUNTRY_CODE
to views viares.locals
. - The routes defined in
routes.js
are mounted. - A basic server listener starts the application on the configured
PORT
.
- We load
2. Implementing Core Functionality (Scheduling & Sending)
Now, let's implement the routing and the logic to handle appointment submissions and schedule SMS messages via Sinch.
// routes.js
const express = require('express');
const router = express.Router();
const { DateTime } = require('luxon'); // For date/time handling
const { SinchClient } = require('@sinch/sdk-core');
require('dotenv').config(); // Ensure env vars are loaded
// --- Sinch Client Initialization ---
// Ensure required environment variables are present
if (!process.env.PROJECT_ID || !process.env.KEY_ID || !process.env.KEY_SECRET || !process.env.SMS_REGION || !process.env.FROM_NUMBER) {
console.error(""FATAL ERROR: Missing required Sinch environment variables in .env file."");
console.error(""Please check KEY_ID, KEY_SECRET, PROJECT_ID, SMS_REGION, FROM_NUMBER."");
process.exit(1); // Exit if essential config is missing
}
const sinchClient = new SinchClient({
projectId: process.env.PROJECT_ID,
keyId: process.env.KEY_ID,
keySecret: process.env.KEY_SECRET,
region: process.env.SMS_REGION, // Use region from .env
});
// --- Helper Function to Send Scheduled SMS ---
async function scheduleReminder(to, message, sendAtIsoString) {
console.log(`Attempting to schedule SMS:`);
console.log(` To: ${to}`);
console.log(` Message: ""${message}""`);
console.log(` Scheduled Send Time (UTC): ${sendAtIsoString}`);
try {
const response = await sinchClient.sms.batches.send({
sendSMSRequestBody: {
to: [to], // Expects an array of recipients
from: process.env.FROM_NUMBER,
body: message,
send_at: sendAtIsoString, // ISO 8601 format UTC timestamp
// Optional: Add delivery report callback URL, expiry, etc.
// delivery_report: 'FULL',
// expire_at: '...',
},
});
console.log(""Sinch API Response:"", JSON.stringify(response, null, 2));
// A successful schedule request doesn't mean SMS sent, just accepted by Sinch.
// The response includes a batch_id which can be used to track status later if needed.
return { success: true, batchId: response.id };
} catch (error) {
console.error(""Error sending/scheduling SMS via Sinch:"", error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
// Provide more context on potential errors
let errorMessage = ""Failed to schedule reminder via Sinch."";
if (error.response && error.response.data && error.response.data.text) {
errorMessage += ` Details: ${error.response.data.text}`;
} else if (error.message) {
errorMessage += ` Details: ${error.message}`;
}
return { success: false, error: errorMessage };
}
}
// --- Route Definitions ---
// GET / : Redirect to the appointment form
router.get('/', (req, res) => {
res.redirect('/appointment');
});
// GET /appointment : Display the appointment scheduling form
router.get('/appointment', (req, res) => {
// Render the form. Flash messages (like error_msg) are available
// globally via res.locals set in app.js, so no need to pass them here.
res.render('patient_details'); // Renders patient_details.ejs
});
// POST /appointment : Handle form submission, validate, and schedule SMS
router.post('/appointment', async (req, res, next) => {
const { patientName, doctorName, appointmentDate, appointmentTime, patientPhone } = req.body;
// --- 1. Input Validation (Basic) ---
if (!patientName || !doctorName || !appointmentDate || !appointmentTime || !patientPhone) {
req.flash('error_msg', 'Please fill in all fields.');
return res.redirect('/appointment');
}
// --- 2. Date/Time Processing with Luxon ---
let appointmentDateTimeLocal;
try {
// Combine date and time, parse using Luxon, assuming local time input from the server's perspective
appointmentDateTimeLocal = DateTime.fromISO(`${appointmentDate}T${appointmentTime}`, { zone: 'local' });
if (!appointmentDateTimeLocal.isValid) {
req.flash('error_msg', `Invalid date or time format. Error: ${appointmentDateTimeLocal.invalidReason || 'Unknown reason'}`);
return res.redirect('/appointment');
}
} catch (parseError) {
console.error(""Error parsing date/time:"", parseError);
req.flash('error_msg', 'Could not parse the provided date or time.');
return res.redirect('/appointment');
}
// --- 3. Scheduling Validation ---
try {
// Ensure appointment is reasonably in the future (e.g., > 2 hours from now)
// Reminder is set 2 hours before appointment
const reminderLeadTime = { hours: 2 };
// User must book slightly before the reminder needs to be scheduled by Sinch (allow buffer)
const minimumBookingLeadTime = { hours: 2, minutes: 5 };
const nowLocal = DateTime.local();
const reminderDateTimeLocal = appointmentDateTimeLocal.minus(reminderLeadTime);
// Check if the *appointment* itself is far enough in the future
if (appointmentDateTimeLocal < nowLocal.plus(minimumBookingLeadTime)) {
req.flash('error_msg'_ `Appointment must be at least ${minimumBookingLeadTime.hours} hours and ${minimumBookingLeadTime.minutes} minutes from now.`);
return res.redirect('/appointment');
}
// --- 4. Prepare Data for Sinch ---
// Format reminder time as UTC ISO 8601 string (required by Sinch send_at)
const reminderDateTimeUtcISO = reminderDateTimeLocal.toUTC().toISO();
// Format phone number (simple prepend - **enhance with validation/parsing library for production**)
let formattedPhone = patientPhone.trim();
// Basic check if it already starts with '+'
if (!formattedPhone.startsWith('+')) {
const defaultCountryCode = process.env.DEFAULT_COUNTRY_CODE || '+1'; // Fallback to +1
formattedPhone = defaultCountryCode + formattedPhone.replace(/[^0-9]/g_ ''); // Remove non-digits
}
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !! PRODUCTION WARNING: Robust phone number validation is crucial! !!
// !! Use a library like 'libphonenumber-js' to parse & validate: !!
// !! `npm install libphonenumber-js` !!
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Construct the SMS message body
const messageBody = `Hi ${patientName}_ this is a reminder for your appointment with Dr. ${doctorName} scheduled for ${appointmentDateTimeLocal.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY)}.`;
// --- 5. Schedule SMS via Sinch Helper ---
const scheduleResult = await scheduleReminder(formattedPhone_ messageBody_ reminderDateTimeUtcISO);
if (!scheduleResult.success) {
req.flash('error_msg'_ `Failed to schedule reminder. ${scheduleResult.error || ''}`);
return res.redirect('/appointment');
}
// --- 6. Persist Data (Database Required for Production!) & Prepare for Success Page ---
// !! IMPORTANT FOR PRODUCTION:
// !! The data below MUST be saved to a persistent database (See Section 6).
// !! Storing it only in the session is NOT sufficient for scheduled jobs_
// !! as session data can be lost.
// !! We store details in the session *temporarily* just to display on the immediate success page.
req.session.appointmentDetails = {
patient: patientName_
doctor: doctorName_
phone: formattedPhone_ // Store formatted number
appointmentDateTimeDisplay: appointmentDateTimeLocal.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY)_
reminderDateTimeDisplay: reminderDateTimeLocal.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY)_
sinchBatchId: scheduleResult.batchId || 'N/A'
};
// In a DB implementation_ you would save the record here and potentially store
// the new record's ID in the session: req.session.appointmentId = dbRecord.id;
req.flash('success_msg'_ 'Appointment reminder scheduled successfully!');
res.redirect('/success');
} catch (error) {
console.error(""Error processing appointment:""_ error);
req.flash('error_msg'_ 'An unexpected error occurred while processing the appointment.');
res.redirect('/appointment');
// Optionally pass to Express error handler: next(error);
}
});
// GET /success : Display confirmation page
router.get('/success'_ (req_ res) => {
// Retrieve data passed temporarily via the session for display purposes.
const appointmentDetails = req.session.appointmentDetails;
// Clear the temporary data from the session after retrieving it
if (req.session.appointmentDetails) {
delete req.session.appointmentDetails;
}
// !! PRODUCTION NOTE:
// !! In a production setup with a database, you would typically retrieve
// !! the appointment details from the DB using an ID passed via query param
// !! or session (e.g., req.query.id or req.session.appointmentId) instead of
// !! relying on data directly in the session object from the previous request.
// !! Example: const appointmentDetails = await prisma.appointment.findUnique({ where: { id: req.session.appointmentId } });
if (!appointmentDetails) {
// If data is missing (e.g., session expired before page load, or user navigated directly)
req.flash('error_msg', 'Confirmation details expired or unavailable. Please check your schedule or contact support.');
return res.redirect('/appointment');
}
res.render('success', { // Renders success.ejs
patient: appointmentDetails.patient,
doctor: appointmentDetails.doctor,
phone: appointmentDetails.phone,
appointmentTimeDisplay: appointmentDetails.appointmentDateTimeDisplay,
reminderTimeDisplay: appointmentDetails.reminderDateTimeDisplay,
batchId: appointmentDetails.sinchBatchId
// success_msg is already available via res.locals
});
});
module.exports = router; // Export the router instance
- Sinch Client: Initialized using credentials from
.env
. Includes a basic check for missing variables. scheduleReminder
Function: Encapsulates the logic for calling the Sinch API (sinchClient.sms.batches.send
). Uses UTC ISO 8601 forsend_at
. Includes basic error handling.- Routes:
/
: Redirects to the main form./appointment
(GET): Renders thepatient_details.ejs
view./appointment
(POST):- Validation: Basic required field checks.
- Date/Time: Uses
luxon
for parsing and validation, assuming server's local time zone for input. - Scheduling Logic: Calculates reminder time (2 hours prior) and checks if appointment is sufficiently in the future.
- Prepare Data: Formats reminder time to UTC ISO 8601. Performs basic phone number formatting with a strong recommendation and comment to use
libphonenumber-js
in production. Constructs the message. - Schedule: Calls
scheduleReminder
. - Store & Redirect: Stores confirmation details temporarily in
req.session.appointmentDetails
solely for display on the immediate success page. Crucially, adds comments emphasizing that this is NOT persistent storage and a database is mandatory for production. Redirects to/success
.
/success
(GET): Retrieves data fromreq.session.appointmentDetails
, renderssuccess.ejs
, and then clears the temporary session data. Includes comments explaining how this would work with a database (fetching by ID). Handles cases where session data might be missing.
3. Building the Web Interface
Create the simple HTML forms for user interaction using EJS templates.
views/patient_details.ejs
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width_ initial-scale=1.0">
<title>Schedule Appointment Reminder</title>
<link rel="stylesheet" href="/css/style.css"> <!-- Link to your CSS -->
</head>
<body>
<h1>Schedule Sinch Hospital Appointment Reminder</h1>
<!-- Display Flash Error Messages -->
<% if (typeof error_msg !== 'undefined' && error_msg && error_msg.length > 0) { %>
<div class="alert alert-danger"><%= error_msg %></div>
<% } %>
<!-- Success messages are shown on the /success page -->
<form action="/appointment" method="POST">
<div>
<label for="patientName">Patient Name:</label>
<input type="text" id="patientName" name="patientName" required>
</div>
<div>
<label for="doctorName">Doctor Name:</label>
<input type="text" id="doctorName" name="doctorName" required>
</div>
<div>
<label for="appointmentDate">Appointment Date:</label>
<input type="date" id="appointmentDate" name="appointmentDate" required>
</div>
<div>
<label for="appointmentTime">Appointment Time:</label>
<input type="time" id="appointmentTime" name="appointmentTime" required>
</div>
<div>
<label for="patientPhone">Patient Phone Number:</label>
<input type="tel" id="patientPhone" name="patientPhone" required placeholder="+15551234567 or 5551234567">
<small>Enter number with country code (e.g., +1555...) or without if in the default region (<%= process.env.DEFAULT_COUNTRY_CODE || '+1' %>).</small>
</div>
<button type="submit">Schedule Reminder</button>
</form>
</body>
</html>
views/success.ejs
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width_ initial-scale=1.0">
<title>Reminder Scheduled</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<h1>Appointment Reminder Scheduled Successfully!</h1>
<!-- Display Flash Success Message -->
<% if (typeof success_msg !== 'undefined' && success_msg && success_msg.length > 0) { %>
<div class="alert alert-success"><%= success_msg %></div>
<% } %>
<% if (typeof patient !== 'undefined' && patient) { %>
<p>An SMS reminder has been scheduled for:</p>
<ul>
<li><strong>Patient:</strong> <%= patient %></li>
<li><strong>Doctor:</strong> <%= doctor %></li>
<li><strong>Appointment Time:</strong> <%= appointmentTimeDisplay %></li>
<li><strong>Patient Phone:</strong> <%= phone %></li>
<li><strong>Reminder Scheduled Time:</strong> <%= reminderTimeDisplay %> (approx, local time)</li>
<li><strong>Sinch Batch ID:</strong> <%= batchId %></li>
</ul>
<p><em>Please note: This confirmation shows details based on the submission. Ensure data is saved persistently for reliability.</em></p>
<% } else { %>
<p>Confirmation details could not be retrieved (session might have expired or data was unavailable).</p>
<% } %>
<p><a href="/appointment">Schedule another reminder</a></p>
</body>
</html>
public/css/style.css
(Basic Styling):
/* public/css/style.css */
body {
font-family: sans-serif;
padding: 20px;
line-height: 1.6;
}
h1 {
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
form div {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
input[type="date"],
input[type="time"],
input[type="tel"] {
width: 95%;
max-width: 400px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
small {
display: block;
font-size: 0.8em;
color: #666;
margin-top: 3px;
}
button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
button:hover {
background-color: #0056b3;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
ul {
list-style: none;
padding: 0;
}
li {
margin-bottom: 8px;
}
- Corrected HTML/EJS file extensions to
.ejs
. - Fixed escaped quotes in HTML attributes (e.g.,
lang="en"
). - Added checks (
typeof variable !== 'undefined'
) in EJS templates for robustness when accessing potentially missing variables like flash messages or session data. patient_details.ejs
: Removed the display block forsuccess_msg
. Updated phone input guidance.success.ejs
: Displays confirmation details retrieved from the session. Includes a note about the temporary nature of this display method.
4. Integrating with Sinch (Summary)
Integration happens primarily in routes.js
:
- Credentials: Securely loaded from
.env
. - Client Initialization: The
SinchClient
is instantiated using the Project ID, Key ID, Key Secret, and Region. - Scheduling API Call: The
scheduleReminder
function usessinchClient.sms.batches.send
, passing the recipient (to
), sender (from
), message body, and the crucialsend_at
parameter (UTC ISO 8601 timestamp).
Obtaining Credentials:
- Log in to your Sinch Customer Dashboard.
- Navigate to Access Keys in the left menu.
- Click Create Key. Give it a name (e.g.,
""NodeReminderApp""
). - Immediately copy the
Key ID
andKey Secret
. The Secret is only shown once. Store these securely in your.env
file. - Your Project ID is usually visible at the top of the dashboard or under API settings. Add it to
.env
. - Navigate to Numbers -> Your Numbers. Ensure you have an active number capable of sending SMS in your desired region. Copy the number (in E.164 format, e.g.,
+1xxxxxxxxxx
) and add it asFROM_NUMBER
in.env
. - Note the Region (
us
,eu
, etc.) your service plan/project is associated with and setSMS_REGION
in.env
.
5. Error Handling and Logging
The current implementation has basic error handling:
- Missing Env Vars:
routes.js
checks for essential Sinch variables on startup and exits if they're missing. - API Call Errors: The
scheduleReminder
function usestry...catch
around thesinchClient.sms.batches.send
call. It logs the error and returns a failure status. - Input Validation: Basic checks for empty fields and appointment time validity in the POST
/appointment
handler. Flash messages inform the user. - General Errors: A fallback
try...catch
in the POST/appointment
handler catches unexpected errors during processing. - Logging: Currently uses
console.log
andconsole.error
. Logs are reasonably consistent.
Production Enhancements:
- Structured Logging: Use a library like
winston
orpino
for structured JSON logs, which are easier to parse and analyze.Replacenpm install winston
console.log
/console.error
with logger instances (e.g.,logger.info()
,logger.error()
). Configure transports to write logs to files or external logging services. - Detailed API Error Parsing: Check the
error.response.data
object from Sinch API errors for specific error codes and messages to provide more informative feedback or trigger specific actions (e.g., retries for transient network issues). - Centralized Error Handling Middleware: Implement more robust Express error-handling middleware in
app.js
to catch unhandled errors gracefully. - Retry Mechanisms: For transient network errors when calling the Sinch API, consider implementing a simple retry strategy (e.g., retry 1-2 times with a short delay). However, since Sinch handles the sending schedule, retrying the scheduling call might result in duplicate scheduled messages if the first call succeeded but the response was lost. Be cautious here. It's often better to log the failure and potentially alert an administrator.
6. Database Schema and Data Layer (Production Requirement)
Critically, using only express-session
to pass data to the success page is NOT persistent storage. Scheduled jobs rely on data that survives server restarts and deployments. If the server restarts between the time an appointment is scheduled via the API and the time the success page is rendered (or if the user's session expires), the confirmation data will be lost. More importantly, the application has no record of the scheduled appointments if the server restarts.
A database is mandatory for any production-level application.
Conceptual Database Schema (e.g., using PostgreSQL or MySQL):
CREATE TABLE appointments (
id SERIAL PRIMARY KEY, -- Or UUID for distributed systems
patient_name VARCHAR(255) NOT NULL,
doctor_name VARCHAR(255) NOT NULL,
patient_phone VARCHAR(30) NOT NULL, -- Store in E.164 format ideally
appointment_datetime TIMESTAMPTZ NOT NULL, -- Store with time zone (UTC recommended)
reminder_datetime TIMESTAMPTZ NOT NULL, -- Store scheduled reminder time (UTC recommended)
sinch_batch_id VARCHAR(100) UNIQUE, -- Store the ID returned by Sinch API for tracking
status VARCHAR(50) DEFAULT 'scheduled', -- e.g., 'scheduled', 'sent', 'failed', 'cancelled'
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Optional: Index for querying by phone or appointment time
CREATE INDEX idx_appointments_phone ON appointments(patient_phone);
CREATE INDEX idx_appointments_datetime ON appointments(appointment_datetime);
Data Layer Implementation Steps:
- Choose a Database: Select a database (e.g., PostgreSQL, MySQL, MongoDB).
- Install ORM/Driver: Install a Node.js database client or Object-Relational Mapper (ORM).
- PostgreSQL:
npm install pg
(driver) ornpm install sequelize
/npm install @prisma/client
(ORMs) - MySQL:
npm install mysql2
(driver) ornpm install sequelize
/npm install @prisma/client
(ORMs) - MongoDB:
npm install mongodb
(driver) ornpm install mongoose
(ODM)
- PostgreSQL:
- Configure Connection: Set up database connection details (preferably using environment variables).
- Define Model/Schema: Create the table/collection schema as shown above using your chosen tool's syntax.
- Modify
routes.js
:- Import: Import your database client/model.
- Save Appointment: In the
POST /appointment
handler, after successfully scheduling the SMS with Sinch (scheduleResult.success
), save the appointment details (includingpatientName
,doctorName
,formattedPhone
,appointmentDateTimeLocal.toJSDate()
,reminderDateTimeLocal.toJSDate()
,scheduleResult.batchId
) to the database. - Pass ID: Instead of storing the full details in
req.session.appointmentDetails
, store only the newly created database record's ID:req.session.appointmentId = newAppointmentRecord.id;
. - Retrieve for Success Page: In the
GET /success
handler, retrieve theappointmentId
from the session (req.session.appointmentId
). Use this ID to query the database for the full appointment details. Render thesuccess.ejs
template using the data fetched from the database. Clearreq.session.appointmentId
afterwards.
- Phone Number Validation: Implement robust phone number validation and formatting using a library like
libphonenumber-js
before saving to the database and sending to Sinch. Ensure numbers are stored consistently (e.g., E.164 format).
This ensures that appointment data is persisted reliably, even if the application restarts, and provides a foundation for future features like viewing scheduled appointments, cancellation, or status tracking.