code examples
code examples
SMS Appointment Reminder System: MessageBird Node.js Tutorial with Express & MongoDB
Build a production-ready SMS appointment reminder system with MessageBird API, Node.js, and Express. Complete tutorial covering phone validation, scheduled SMS delivery, MongoDB integration, timezone handling, and deployment best practices for automated appointment notifications.
Reduce no-shows and enhance customer experience by automatically sending SMS appointment reminders. Missed appointments cost businesses time and revenue. A timely SMS reminder is a simple, effective way to ensure customers remember their commitments.
This comprehensive tutorial shows you how to build a production-ready SMS appointment reminder system using Node.js, Express, and the MessageBird API. Learn phone number validation with MessageBird Lookup, scheduled SMS delivery, MongoDB integration, timezone handling, error handling, and deployment strategies.
How to Build an SMS Appointment Reminder System with MessageBird
What We'll Build:
A Node.js web application using the Express framework that:
- Provides an API endpoint to accept appointment details (customer name, phone number, appointment date/time).
- Validates the phone number using MessageBird Lookup.
- Schedules an SMS reminder via the MessageBird API to be sent a configurable time (e.g., 3 hours) before the appointment.
- Stores appointment information in a database (MongoDB).
- Includes essential production considerations like error handling, logging, security, and basic monitoring.
Problem Solved: Automating appointment reminders to minimize costly no-shows and improve operational efficiency.
Technologies Used:
- Node.js: A JavaScript runtime for building server-side applications.
- Express: A minimal and flexible Node.js web application framework.
- MessageBird: A communications platform-as-a-service (CPaaS) for sending SMS, performing number lookups, and more.
- MongoDB: A NoSQL database for storing appointment data (using Mongoose ODM).
- Moment.js / Moment Timezone: Libraries for easy date/time manipulation and time zone handling. Note: Moment.js is deprecated since September 2020. For new projects, consider date-fns (modular, tree-shakable, ~300 bytes per function) or Day.js (2KB, Moment.js-compatible API).
- dotenv: For managing environment variables securely.
- express-validator: For robust input validation.
- winston: For flexible logging.
- express-rate-limit: For basic API rate limiting.
- Helmet: For securing Express apps with various HTTP headers.
Architecture:
graph LR
A[User/Client App] -- POST /api/appointments --> B(Node.js/Express App);
B -- Validate & Store --> C{MongoDB};
B -- Lookup Number --> D(MessageBird Lookup API);
B -- Schedule SMS --> E(MessageBird SMS API);
E -- Sends SMS at Scheduled Time --> F[Customer's Phone];Prerequisites:
- Node.js (v22 LTS recommended for 2025, minimum v18 required for Express 5.x) and npm (or yarn) installed. Install Node.js.
- A MessageBird account and an API Key. Sign up for MessageBird.
- Access to a MongoDB instance (local or cloud-based like MongoDB Atlas). Install MongoDB or Use Atlas.
- Basic understanding of JavaScript, Node.js, REST APIs, and databases.
- A text editor or IDE (like VS Code).
- A tool for testing APIs (like
curlor Postman).
Important Version Notes:
- Node.js 22 LTS became LTS in October 2024, providing active support until October 21, 2025 and maintenance until April 30, 2027.
- Express 5.0 was released October 15, 2024 and requires Node.js 18 or higher. This guide uses Express 4.x for broader compatibility.
- Moment.js has been deprecated since September 2020. Production applications should migrate to date-fns (modular, tree-shakable, ~300 bytes per function) or Day.js (2KB, Moment.js-compatible API) for better performance and ongoing support.
- MessageBird Node.js SDK v4.0.1 is the current stable version (released January 2022).
Expected Outcome: A functional API endpoint capable of receiving appointment data, validating it, storing it, and scheduling an SMS reminder via MessageBird.
1. Setting up the Project
Let's initialize our Node.js project and install necessary dependencies.
Step 1: Create Project Directory and Initialize
Open your terminal and run:
mkdir messagebird-reminders
cd messagebird-reminders
npm init -yThis creates a package.json file with default settings.
Step 2: Install Dependencies
npm install express messagebird dotenv moment moment-timezone mongoose express-validator winston express-rate-limit helmetexpress: Web framework.messagebird: Official MessageBird Node.js SDK.dotenv: Loads environment variables from a.envfile.moment,moment-timezone: Date/time handling and time zones.mongoose: MongoDB object modeling tool.express-validator: Input validation middleware.winston: Logging library.express-rate-limit: Basic rate limiting middleware.helmet: Security middleware.
Step 3: Install Development Dependencies (Optional but Recommended)
npm install --save-dev nodemonnodemon: Automatically restarts the server during development when files change.
Step 4: Configure nodemon (Optional)
Add a script to your package.json for easy development startup. Update the main entry point as well.
{
"name": "messagebird-reminders",
"version": "1.0.0",
"description": "",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-validator": "^7.0.1",
"helmet": "^7.1.0",
"messagebird": "^4.0.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"mongoose": "^8.0.3",
"winston": "^3.11.0"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}Step 5: Set up Project Structure
Create the following directory structure:
messagebird-reminders/
├── config/
│ └── logger.js
├── models/
│ └── Appointment.js
├── routes/
│ └── api.js
├── controllers/
│ └── appointmentController.js
├── middleware/
│ ├── errorHandler.js
│ └── validation.js
├── src/
│ └── server.js
├── .env
├── .gitignore
└── package.json
config/: Configuration files (e.g., logger setup).models/: Database models (Mongoose schemas).routes/: Express route definitions.controllers/: Logic to handle requests (separating concerns from routes).middleware/: Custom middleware functions (error handling, validation).src/: Main application source code..env: Stores environment variables (API keys, database URIs). Do not commit this file to Git..gitignore: Specifies intentionally untracked files that Git should ignore (likenode_modules,.env).
Step 6: Create .gitignore
Create a .gitignore file in the project root:
# .gitignore
# Dependencies
node_modules/
# Environment variables
.env
# Logs
logs/
*.log
# OS generated files
.DS_Store
Thumbs.dbStep 7: Create .env File
Create a .env file in the project root. Remember to replace placeholders with your actual credentials.
# .env
# Server Configuration
PORT=8080
NODE_ENV=development
# MessageBird Configuration
MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_API_KEY
MESSAGEBIRD_ORIGINATOR="YourBrand" # Or a purchased Virtual Mobile Number e.g. +12025550144
DEFAULT_COUNTRY_CODE=US # Default ISO 3166-1 alpha-2 country code for number lookup
# MongoDB Configuration
MONGO_URI=mongodb://localhost:27017/reminderApp # Or your MongoDB Atlas connection string
# Reminder Configuration
REMINDER_HOURS_BEFORE=3 # How many hours before the appointment to send the reminder
BUSINESS_TIMEZONE=America/New_York # Default timezone for appointment interpretationMESSAGEBIRD_API_KEY: Get this from the MessageBird Dashboard -> Developers -> API access (REST). Use your live key.MESSAGEBIRD_ORIGINATOR: This is the sender ID shown on the SMS. It can be an alphanumeric string (up to 11 characters, check country restrictions) or a virtual mobile number purchased from MessageBird. Note: Alphanumeric sender IDs are not supported in the US or Canada; you must use a Toll-Free Number or 10DLC registered number for these countries.DEFAULT_COUNTRY_CODE: Used by MessageBird Lookup to help parse local phone numbers. Use the code relevant to your primary audience.MONGO_URI: Your MongoDB connection string.REMINDER_HOURS_BEFORE: Defines when the reminder is sent relative to the appointment.BUSINESS_TIMEZONE: Crucial for interpreting appointment times correctly if not explicitly provided with an offset. Use a valid IANA time zone name.
Step 8: Basic Server Setup
Create the main server file src/server.js:
// src/server.js
require('dotenv').config(); // Load environment variables first
const express = require('express');
const helmet = require('helmet');
const mongoose = require('mongoose');
const logger = require('../config/logger');
const apiRoutes = require('../routes/api');
const errorHandler = require('../middleware/errorHandler');
const app = express();
const PORT = process.env.PORT || 8080;
// --- Database Connection ---
// Connect to MongoDB. Mongoose v6+ no longer requires deprecated options.
mongoose.connect(process.env.MONGO_URI)
.then(() => logger.info('MongoDB Connected successfully.'))
.catch(err => {
logger.error('MongoDB Connection Error:', err);
process.exit(1); // Exit if cannot connect to DB
});
// --- Middleware ---
app.use(helmet()); // Basic security headers
app.use(express.json()); // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// --- Routes ---
app.use('/api', apiRoutes); // Mount API routes
// Health check endpoint
app.get('/health', (req, res) => res.status(200).send('OK'));
// --- Error Handling ---
// Not found handler (404) - place after all routes
app.use((req, res, next) => {
res.status(404).json({ message: 'Resource not found' });
});
// Centralized error handler - place last
app.use(errorHandler);
// --- Start Server ---
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT} in ${process.env.NODE_ENV} mode`);
});
module.exports = app; // Export for potential testingThis sets up Express, connects to MongoDB, applies basic middleware, defines a health check, sets up error handling placeholders, and starts the server.
2. Implementing Phone Validation and SMS Scheduling
Now, let's build the logic for scheduling appointments with MessageBird.
Step 1: Define the Appointment Model
Create models/Appointment.js to define the structure of appointment documents in MongoDB.
// models/Appointment.js
const mongoose = require('mongoose');
const appointmentSchema = new mongoose.Schema({
customerName: {
type: String,
required: true,
trim: true,
},
phoneNumber: { // Store the validated, international format number
type: String,
required: true,
trim: true,
},
appointmentTime: { // Store as UTC Date object
type: Date,
required: true,
},
reminderTime: { // Store as UTC Date object
type: Date,
required: true,
},
timeZone: { // Store the interpreted timezone for context
type: String,
required: true,
},
messageBirdMessageId: { // ID of the scheduled message from MessageBird
type: String,
required: false, // Might not get it immediately or if scheduling fails
},
status: { // Track status (e.g., scheduled, sent, failed)
type: String,
enum: ['scheduled', 'sending_failed', 'sent', 'canceled'],
default: 'scheduled',
},
createdAt: {
type: Date,
default: Date.now,
},
});
// Index appointmentTime for potential future querying
appointmentSchema.index({ appointmentTime: 1 });
appointmentSchema.index({ reminderTime: 1 });
appointmentSchema.index({ status: 1 });
const Appointment = mongoose.model('Appointment', appointmentSchema);
module.exports = Appointment;Step 2: Create the Appointment Controller
Create controllers/appointmentController.js to handle the business logic.
// controllers/appointmentController.js
const moment = require('moment-timezone');
const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY);
const Appointment = require('../models/Appointment');
const logger = require('../config/logger');
const REMINDER_HOURS = parseInt(process.env.REMINDER_HOURS_BEFORE || '3', 10);
const BUSINESS_TIMEZONE = process.env.BUSINESS_TIMEZONE || 'UTC';
const DEFAULT_COUNTRY_CODE = process.env.DEFAULT_COUNTRY_CODE || null; // Pass null if not set
exports.scheduleAppointment = async (req, res, next) => {
const { customerName, phoneNumber, appointmentDate, appointmentTime, timeZone } = req.body;
// --- 1. Input Validation (Basic - Use express-validator for robust validation) ---
// We assume express-validator has already run (see Section 3)
// --- 2. Date and Time Processing ---
const effectiveTimeZone = timeZone || BUSINESS_TIMEZONE; // Use provided timezone or default
const dateTimeString = `${appointmentDate} ${appointmentTime}`;
let appointmentMoment;
try {
// Parse the date/time string using the specified timezone
appointmentMoment = moment.tz(dateTimeString, "YYYY-MM-DD HH:mm", effectiveTimeZone);
if (!appointmentMoment.isValid()) {
logger.warn(`Invalid date/time format received: ${dateTimeString} for timezone ${effectiveTimeZone}`);
return res.status(400).json({ message: 'Invalid date or time format. Use YYYY-MM-DD and HH:mm.' });
}
// Check if the appointment is in the future (add buffer, e.g., 5 mins for processing)
const minimumViableTime = moment().add(REMINDER_HOURS, 'hours').add(5, 'minutes');
if (appointmentMoment.isBefore(minimumViableTime)) {
logger.warn(`Appointment time too soon: ${appointmentMoment.format()} requested by ${phoneNumber}`);
return res.status(400).json({ message: `Appointment must be at least ${REMINDER_HOURS} hours and 5 minutes in the future.` });
}
} catch (error) {
logger.error(`Error parsing date/time: ${error.message}`, { dateTimeString, effectiveTimeZone });
return next(new Error('Failed to process appointment time.')); // Pass to central error handler
}
const reminderMoment = appointmentMoment.clone().subtract(REMINDER_HOURS, 'hours');
// --- 3. Phone Number Validation (Lookup API) ---
let validatedPhoneNumber;
try {
const lookupResponse = await new Promise((resolve, reject) => {
messagebird.lookup.read(phoneNumber, DEFAULT_COUNTRY_CODE, (err, response) => {
if (err) {
// Specific error for invalid number format
if (err.errors && err.errors[0].code === 21) {
logger.warn(`Invalid phone number format: ${phoneNumber}`, { error: err });
return reject({ status: 400, message: 'Invalid phone number format provided.' });
}
// Other lookup errors
logger.error(`MessageBird Lookup error for ${phoneNumber}:`, err);
return reject({ status: 500, message: 'Failed to validate phone number.' });
}
resolve(response);
});
});
// Check if the number is mobile (or potentially fixed-line-sms if needed)
if (lookupResponse.type !== 'mobile') {
logger.warn(`Non-mobile number provided: ${phoneNumber}, type: ${lookupResponse.type}`);
return res.status(400).json({ message: 'Please provide a valid mobile phone number for SMS reminders.' });
}
validatedPhoneNumber = lookupResponse.phoneNumber; // Use the normalized, international format
} catch (error) {
// Handle specific errors passed from the Promise rejection
if (error.status) {
return res.status(error.status).json({ message: error.message });
}
// Generic fallback
logger.error(`Unexpected error during phone validation: ${error.message}`, { phoneNumber });
return next(new Error('Failed during phone number validation.'));
}
// --- 4. Schedule SMS with MessageBird ---
// Note: Using MESSAGEBIRD_ORIGINATOR directly here. Consider a separate BUSINESS_NAME env var for more clarity if needed.
const messageBody = `${customerName}, this is a reminder for your appointment on ${appointmentMoment.format('MMM D, YYYY')} at ${appointmentMoment.format('h:mm A z')}. See you soon at ${process.env.MESSAGEBIRD_ORIGINATOR}!`;
let messageBirdResponse;
try {
messageBirdResponse = await new Promise((resolve, reject) => {
messagebird.messages.create({
originator: process.env.MESSAGEBIRD_ORIGINATOR,
recipients: [validatedPhoneNumber],
scheduledDatetime: reminderMoment.toISOString(), // Use ISO 8601 format
body: messageBody,
// reference: `appointment_${new Date().getTime()}` // Optional: Add your own reference
}, (err, response) => {
if (err) {
logger.error(`MessageBird scheduling error for ${validatedPhoneNumber}:`, err);
return reject(err); // Let the outer catch handle detailed logging/response
}
logger.info(`Successfully scheduled reminder for ${validatedPhoneNumber} at ${reminderMoment.format()}`, { messageId: response.id });
resolve(response);
});
});
} catch (error) {
logger.error(`Failed to schedule message via MessageBird: ${error.message}`, { phoneNumber: validatedPhoneNumber });
// Don't save appointment if scheduling failed crucial step
// Potentially implement retry logic here or flag for manual intervention
return next(new Error('Failed to schedule reminder SMS. Appointment not saved.'));
}
// --- 5. Store Appointment in Database ---
try {
const newAppointment = new Appointment({
customerName,
phoneNumber: validatedPhoneNumber, // Store validated number
appointmentTime: appointmentMoment.toDate(), // Store as native Date (UTC)
reminderTime: reminderMoment.toDate(), // Store as native Date (UTC)
timeZone: effectiveTimeZone, // Store the timezone used
messageBirdMessageId: messageBirdResponse?.id, // Store MessageBird message ID
status: 'scheduled',
});
await newAppointment.save();
logger.info(`Appointment saved successfully for ${validatedPhoneNumber}`, { id: newAppointment._id });
res.status(201).json({
message: 'Appointment scheduled successfully!',
appointmentId: newAppointment._id,
reminderTime: reminderMoment.format(),
messageBirdDetails: {
id: messageBirdResponse?.id,
status: messageBirdResponse?.recipients?.items[0]?.status // Initial status from MB
}
});
} catch (dbError) {
logger.error(`Database error saving appointment for ${validatedPhoneNumber}: ${dbError.message}`, { error: dbError });
// Critical: Scheduling succeeded, but DB save failed.
// Implement cleanup or alert: Potentially try to cancel the scheduled MessageBird message if possible,
// or flag this for manual review.
// For now, return an error indicating partial failure.
return next(new Error('Reminder scheduled, but failed to save appointment details. Please contact support.'));
}
};This controller handles:
- Parsing and validating dates/times using
moment-timezone. - Validating phone numbers using MessageBird Lookup.
- Scheduling the SMS via
messagebird.messages.createwithscheduledDatetime. - Saving the appointment details to MongoDB.
- Basic error handling and logging.
3. Building a Complete API Layer
Let's define the API endpoint and add robust validation.
Step 1: Create API Routes
Create routes/api.js:
// routes/api.js
const express = require('express');
const appointmentController = require('../controllers/appointmentController');
const { validateAppointment } = require('../middleware/validation');
const rateLimit = require('express-rate-limit');
const router = express.Router();
// Apply rate limiting to the API
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
router.use(apiLimiter);
// POST /api/appointments - Schedule a new appointment reminder
router.post(
'/appointments',
validateAppointment, // Apply validation middleware first
appointmentController.scheduleAppointment
);
// Add other API routes here if needed (e.g., GET /appointments, DELETE /appointments/:id)
module.exports = router;Step 2: Implement Request Validation Middleware
Create middleware/validation.js:
// middleware/validation.js
const { body, validationResult } = require('express-validator');
const moment = require('moment-timezone');
const logger = require('../config/logger');
exports.validateAppointment = [
// Validate customerName: not empty, trimmed
body('customerName')
.trim()
.notEmpty().withMessage('Customer name is required.'),
// Validate phoneNumber: not empty, basic check (more thorough check via Lookup)
body('phoneNumber')
.trim()
.notEmpty().withMessage('Phone number is required.'),
// .isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format.'), // Optional: basic format check
// Validate appointmentDate: is date format YYYY-MM-DD
body('appointmentDate')
.isDate({ format: 'YYYY-MM-DD' }).withMessage('Invalid date format. Use YYYY-MM-DD.'),
// Validate appointmentTime: is time format HH:mm (24-hour)
body('appointmentTime')
.matches(/^([01]\d|2[0-3]):([0-5]\d)$/).withMessage('Invalid time format. Use HH:mm (24-hour).'),
// Validate timeZone: optional, but if provided, must be a valid IANA zone
body('timeZone')
.optional()
.custom((value) => {
if (!moment.tz.zone(value)) {
throw new Error('Invalid time zone provided.');
}
return true;
}),
// Middleware to handle validation results
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
logger.warn('Validation failed for /api/appointments', { errors: errors.array(), body: req.body });
return res.status(400).json({ errors: errors.array() });
}
next(); // Proceed to the controller if validation passes
},
];This uses express-validator to define rules for each expected field in the request body and returns a 400 error with details if validation fails.
Step 3: Testing the API Endpoint
You can now test the endpoint using curl or Postman.
curl Example:
(Make sure your server is running: npm run dev)
curl -X POST http://localhost:8080/api/appointments \
-H "Content-Type: application/json" \
-d '{
"customerName": "Jane Doe",
"phoneNumber": "+12025550125",
"appointmentDate": "2025-06-15",
"appointmentTime": "14:30",
"timeZone": "America/New_York"
}'Expected Success Response (201 Created):
{
"message": "Appointment scheduled successfully!",
"appointmentId": "60a7c8d0e1b2c3a4b5d6e7f8",
"reminderTime": "2025-06-15T11:30:00-04:00",
"messageBirdDetails": {
"id": "mb_message_id_string",
"status": "scheduled"
}
}Example Validation Error Response (400 Bad Request):
If you send invalid data (e.g., missing customerName):
{
"errors": [
{
"type": "field",
"msg": "Customer name is required.",
"path": "customerName",
"location": "body"
}
]
}4. Integrating with Necessary Third-Party Services (MessageBird)
We've already integrated MessageBird, but let's recap the configuration and key acquisition.
-
API Key (
MESSAGEBIRD_API_KEY):- Purpose: Authenticates your requests to the MessageBird API.
- How to Obtain:
- Log in to your MessageBird Dashboard.
- Navigate to Developers in the left-hand menu.
- Click on the API access (REST) tab.
- If you don't have a live key, click Add access key.
- Copy the generated Live API Key.
- Format: A string of alphanumeric characters (e.g.,
live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx). - Storage: Store securely in your
.envfile. Never commit it to version control.
-
Originator (
MESSAGEBIRD_ORIGINATOR):- Purpose: The sender ID displayed on the recipient's phone.
- How to Obtain/Set:
- Alphanumeric: Choose a short brand name (e.g.,
"BeautyBird","ClinicAppt"). Maximum length is 11 characters. Important: Alphanumeric sender IDs are not supported in the US or Canada. For US/Canada, you must use a Toll-Free Number or 10DLC registered number. Check MessageBird's country-specific documentation for restrictions in other countries. - Virtual Mobile Number (VMN): Purchase a number from MessageBird (Dashboard -> Numbers -> Buy a number). This is required for two-way communication and in countries that don't support alphanumeric senders.
- Alphanumeric: Choose a short brand name (e.g.,
- Format: Either an alphanumeric string (up to 11 chars) or a phone number in E.164 format (e.g.,
+12025550144). - Storage: Store in your
.envfile.
-
Default Country Code (
DEFAULT_COUNTRY_CODE):- Purpose: Helps the MessageBird Lookup API correctly parse phone numbers provided without a country code (e.g.,
(202) 555-0125instead of+12025550125). - How to Obtain/Set: Use the ISO 3166-1 alpha-2 code for your primary target country (e.g.,
US,GB,NL,AU). Find codes here. - Format: Two-letter uppercase string.
- Storage: Store in your
.envfile.
- Purpose: Helps the MessageBird Lookup API correctly parse phone numbers provided without a country code (e.g.,
Fallback Mechanisms:
- Lookup Failure: The code currently handles lookup errors by returning a 400 or 500 status to the client. A more robust system might queue the request for a retry if the failure seems temporary (e.g., network issue vs. invalid number format).
- Scheduling Failure: If
messagebird.messages.createfails, the appointment is not saved in the database. This prevents inconsistencies. Consider:- Implementing retry logic with exponential backoff for transient errors (e.g., network timeouts).
- Alerting administrators if scheduling fails consistently.
- MessageBird Service Outage: If MessageBird is down when scheduling, the request will fail. If MessageBird experiences issues after a message is successfully scheduled, the delivery relies on their infrastructure. Monitor MessageBird's status page during critical operations.
5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
Step 1: Centralized Error Handling Middleware
Create middleware/errorHandler.js:
// middleware/errorHandler.js
const logger = require('../config/logger');
const errorHandler = (err, req, res, next) => {
// Log the error internally
logger.error(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`, {
error: {
message: err.message,
stack: err.stack,
status: err.status,
},
request: { // Optional: Log request details for debugging
body: req.body,
params: req.params,
query: req.query,
headers: req.headers,
}
});
// Determine status code - use err.status if available, otherwise default to 500
const statusCode = err.status || 500;
// Send generic error response to the client
// Avoid sending detailed error messages/stack traces in production
const responseMessage = (process.env.NODE_ENV === 'production' && statusCode === 500)
? 'An unexpected internal server error occurred.'
: err.message; // Send more details in development or for non-500 errors
res.status(statusCode).json({
message: responseMessage,
// Optionally include error code or type for client-side handling
// error_code: err.code || 'INTERNAL_ERROR'
});
};
module.exports = errorHandler;This middleware catches errors passed via next(err), logs them, and sends a standardized JSON error response. It avoids leaking stack traces in production.
Step 2: Configure Logging
Create config/logger.js using Winston:
// config/logger.js
const winston = require('winston');
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }), // Log stack traces
winston.format.splat(),
winston.format.json() // Log in JSON format
);
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', // More verbose in dev
format: logFormat,
defaultMeta: { service: 'reminder-service' },
transports: [
// Log errors to a separate file
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
// Log all levels to another file
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
// If we're not in production, also log to the console
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(), // Add colors
winston.format.simple() // Simple format for console
),
}));
}
module.exports = logger;This sets up logging to files (logs/error.log, logs/combined.log) and to the console during development. Remember to create the logs directory or ensure your deployment process does.
Step 3: Using the Logger
Import and use the logger in controllers and other modules as shown in appointmentController.js and server.js:
const logger = require('../config/logger');
logger.info('Informational message');
logger.warn('Warning message');
logger.error('Error message', { error: someErrorObject });Step 4: Retry Mechanisms (Conceptual)
While retrying the scheduled message delivery is MessageBird's responsibility, you might retry certain steps within your controller:
- Database Connection: Mongoose handles some level of retry internally. Ensure your application exits gracefully if it can't establish an initial connection (as done in
server.js). - MessageBird API Calls (Lookup/Scheduling): For transient errors (network issues, temporary MessageBird hiccups), implement a simple retry strategy.
Example Retry Logic Snippet (Conceptual - place inside catch blocks):
// Conceptual retry logic within a catch block
let retries = 3;
let delay = 1000; // Start with 1 second delay
async function attemptAction() {
try {
// Perform the MessageBird API call or DB operation
// ... await messagebird.lookup.read(...) or await newAppointment.save()
} catch (error) {
if (isRetryableError(error) && retries > 0) {
retries--;
logger.warn(`Action failed, retrying in ${delay / 1000}s (${retries} retries left)`, { error });
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
await attemptAction(); // Recursive call
} else {
logger.error('Action failed after retries or with non-retryable error', { error });
throw error; // Re-throw the error to be handled by the main error handler
}
}
}
function isRetryableError(error) {
// Define logic to check if the error suggests a temporary issue
// e.g., check error codes, status codes (5xx), network error types
const statusCode = error?.errors?.[0]?.code || error?.status;
// Simplistic example: retry on undefined status or 5xx errors
return statusCode === undefined || (typeof statusCode === 'number' && statusCode >= 500);
}
// Initial call within your main logic block where the action is performed
try {
await attemptAction();
} catch (finalError) {
// Handle final failure after retries (e.g., pass to next(finalError))
return next(finalError);
}Important: Implement retries carefully to avoid infinite loops and excessive delays. Only retry errors that are likely temporary.
Testing Error Scenarios:
- Provide invalid input data to trigger validation errors.
- Temporarily use an invalid
MESSAGEBIRD_API_KEYto test authentication errors. - Provide deliberately malformed phone numbers (
123) to test Lookup error code 21. - Provide non-mobile numbers (if you know one) to test the
type !== 'mobile'check. - Set an appointment time in the past or too close to the present.
- Temporarily stop your MongoDB instance to test database connection/save errors.
- (Harder) Simulate network interruptions or specific MessageBird 5xx errors (often requires mocking).
6. Creating a Database Schema and Data Layer
We defined the Mongoose schema in models/Appointment.js (Section 2, Step 1).
- Schema Definition: Included fields for customer details, validated phone number, appointment/reminder times (as UTC Dates), timezone context, MessageBird message ID, and status.
- Data Access: Mongoose provides the data access layer. We use
new Appointment({...})andawait newAppointment.save()in the controller. - Migrations: For simple schema changes during development, dropping the collection might suffice. In production, use a migration tool (like
migrate-mongooseor handle schema evolution within your application logic if possible) to manage changes without data loss. - Performance: Added indexes on
appointmentTime,reminderTime, andstatusfor efficient querying, especially if you need to find upcoming reminders or check statuses frequently.
Frequently Asked Questions (FAQ)
How do I integrate MessageBird SMS API with React or Vue.js frontend applications?
MessageBird SMS integration with React or Vue.js requires a backend API (Node.js/Express) to handle the MessageBird SDK since API keys should never be exposed in frontend code. Your React/Vue frontend sends appointment data to the backend endpoint via HTTP POST using Axios or Fetch API, and the backend processes the request using the MessageBird Node.js SDK. This architecture keeps your API credentials secure on the server side while allowing your frontend to trigger SMS functionality. For more on frontend integration patterns, see our guides on building SMS systems with React and Vue.js SMS applications.
What is the MessageBird API rate limit for SMS scheduling?
According to the MessageBird SMS API documentation, the rate limits are: GET requests: 50 req/s, POST requests: 500 req/s, PATCH requests: 50 req/s, DELETE requests: 50 req/s. These limits apply per API key. When you receive a 429 Too Many Requests response, reduce your request rate and implement exponential backoff retry logic. Use rate limiting middleware (express-rate-limit) in your application to prevent exceeding these limits.
How do I validate phone numbers before sending SMS with MessageBird?
Use the MessageBird Lookup API (messagebird.lookup.read()) to validate phone numbers. This API checks if the number is valid, returns the normalized E.164 format (international format without plus sign prefix), identifies the number type (mobile, landline, VOIP, etc.), and provides carrier information. The API performs an HLR (Home Location Register) lookup on the mobile network in real-time. Always validate phone numbers before scheduling SMS to ensure deliverability and avoid wasting API credits on invalid numbers.
Can I use MessageBird for free or do I need a paid plan?
MessageBird offers 10 free test SMS credits when you sign up and verify your phone number. These test credits can only send messages to your verified number. For production use and sending to other numbers, you need to purchase credits or subscribe to a paid plan. SMS pricing typically ranges from $0.008 per outbound SMS message in the US, with additional costs for dedicated numbers ($0.50/month for VMN, $700/month for shortcodes). Pricing varies by destination country.
What's the difference between scheduledDatetime and immediate SMS sending?
The scheduledDatetime parameter in messagebird.messages.create() allows you to specify a future timestamp (in RFC3339/ISO 8601 format: Y-m-d\TH:i:sP) when the SMS should be sent. Without this parameter, MessageBird sends the SMS immediately. MessageBird stores scheduled messages and sends them at the specified time. Use scheduled sending for appointment reminders, time-sensitive notifications, or campaigns that need to respect recipient time zones. You can view and manage scheduled messages via the Dashboard or API.
Which is better: date-fns or Day.js to replace Moment.js?
Both date-fns and Day.js are excellent Moment.js alternatives. Date-fns is modular and tree-shakable, reducing bundle size significantly (each function is ~300 bytes). It uses functional programming patterns and works well with modern build tools. Day.js offers a Moment.js-compatible API, making migration easier with minimal code changes. It's extremely lightweight (2KB) and uses a plugin system for extended functionality. Choose date-fns for modern projects prioritizing bundle size and functional programming, or Day.js for easy migration from Moment.js with minimal refactoring.
How do I handle time zones correctly in SMS appointment reminders?
Store all appointment times in UTC (as Date objects) in your database and include the original timezone string for context. Use moment-timezone (or date-fns-tz/Luxon) to parse user input in their local timezone, convert to UTC for storage, and format back to their timezone for display. Set a default business timezone in your .env file (BUSINESS_TIMEZONE=America/New_York) to interpret times when users don't specify a timezone. Use IANA time zone names (e.g., America/New_York, not abbreviations like EST). Always include timezone information in reminder messages to avoid confusion.
What MongoDB indexes should I create for appointment reminders?
Create indexes on frequently queried fields: appointmentTime (ascending), reminderTime (ascending), and status (ascending). These indexes optimize queries when searching for upcoming appointments, scheduled reminders, or filtering by status (scheduled, sent, failed). Use compound indexes like { status: 1, reminderTime: 1 } if you frequently query by status AND time together. The code in this guide creates these indexes automatically via Mongoose schema definitions using appointmentSchema.index().
How do I test MessageBird SMS scheduling without sending real messages?
MessageBird provides a Test API Key (starts with test_) available in your Dashboard under "Developers" → "API access". Messages sent with test keys appear in your MessageBird Dashboard logs but are not delivered to real phone numbers. Test keys allow you to verify your integration logic, error handling, and data flow without incurring costs or sending actual SMS messages. You also receive 10 free test credits that can only send to your verified phone number. Always test with the test key before switching to your live API key in production.
Conclusion
Building an SMS appointment reminder system with Node.js, Express, and MessageBird reduces no-shows, improves customer communication, and automates time-consuming manual reminder processes. This guide demonstrated implementing phone validation, scheduled SMS delivery, database integration, and production-ready error handling.
Key takeaways:
- Use MessageBird Lookup API to validate phone numbers before scheduling SMS, ensuring deliverability and E.164 format consistency
- Store all appointment times in UTC using Date objects, and preserve the original timezone for accurate display and reminder calculations
- Implement the
scheduledDatetimeparameter in RFC3339 format to schedule SMS delivery at specific future times - Migrate from deprecated Moment.js to date-fns or Day.js for better performance and ongoing support
- Apply security best practices: environment variables for API keys, Helmet middleware, rate limiting, and input validation
- Use Node.js 22 LTS (supported until April 2027) for production applications requiring long-term stability
- Be aware of sender ID restrictions: alphanumeric sender IDs are not supported in US/Canada; use Toll-Free or 10DLC numbers instead
- Understand MessageBird API rate limits: 500 req/s for POST requests, with 429 errors returned when exceeded
- Create database indexes on
appointmentTime,reminderTime, andstatusfor optimal query performance - Implement centralized error handling with Winston logging to track failures and debug issues in production
Next steps for production:
- Add webhook handlers to process MessageBird delivery reports and update appointment status in your database
- Implement message template management for different reminder types (initial reminder, follow-up, cancellation)
- Set up monitoring and alerting (using tools like New Relic, Datadog, or Sentry) to track API failures and delivery rates
- Add pagination and filtering to appointment management endpoints for administrative dashboards
- Implement retry queues (using Bull, BullMQ, or AWS SQS) for failed SMS deliveries with exponential backoff
- Add support for multiple languages and time zones using i18n libraries
- Create automated tests (unit tests with Jest, integration tests with Supertest) to ensure reliability
- Consider implementing a frontend dashboard using React, Vue, or your preferred framework to manage appointments visually
- For US/Canada deployments, register your 10DLC campaigns or Toll-Free numbers to ensure message deliverability and compliance
Related topics:
- E.164 phone number format standards and international phone validation
- Webhook implementation for SMS delivery status tracking
- Time zone handling best practices in distributed systems
- MongoDB schema design patterns for time-series data
- Node.js production deployment strategies (Docker, Kubernetes, serverless)
- SMS compliance and regulations (TCPA, GDPR, opt-in requirements)
- Alternative SMS providers comparison (Twilio, AWS SNS, Vonage)
- Building two-way SMS communication systems with MessageBird
Frequently Asked Questions
How to send SMS appointment reminders with Node.js?
Use Node.js with Express, MessageBird API, and MongoDB to build an SMS reminder system. This involves setting up a Node.js project, installing necessary libraries like 'messagebird', 'moment-timezone', and 'mongoose', and configuring API keys and database connections. The application will accept appointment details via an API endpoint and schedule SMS reminders through MessageBird.
What is MessageBird used for in appointment reminders?
MessageBird is a communications platform that handles sending SMS messages, validating phone numbers with its Lookup API, and more. It's a key component for delivering the SMS reminders and ensuring accurate phone number formatting.
Why does the project use Moment Timezone?
Moment Timezone is essential for handling different time zones accurately. This is crucial for scheduling reminders based on the appointment time and the business's or client's specified time zone, avoiding incorrect reminder times.
When should I use express-validator in my project?
Express-validator is used for input validation in the API endpoint. It ensures data integrity by checking the format and content of incoming data, preventing issues caused by incorrect or malicious data submissions.
Can I use a different database instead of MongoDB?
While the guide uses MongoDB with Mongoose, you could potentially adapt the code to work with other databases. You'd need to change the database connection and data access methods in the application logic to suit your chosen database.
How to validate phone numbers using MessageBird?
Use the MessageBird Lookup API to validate and normalize phone numbers. This ensures that numbers are correctly formatted and suitable for SMS delivery. The code provides an example using the MessageBird Node.js SDK to interact with the Lookup API, along with specific error handling based on result codes and status.
What is the purpose of scheduledDatetime in MessageBird?
The `scheduledDatetime` parameter allows you to specify when an SMS should be sent in the future, which is central to scheduling appointment reminders in advance using MessageBird. The API accepts the time in ISO 8601 format.
Why is error handling important in a production app?
Proper error handling helps prevent crashes and provides informative responses to clients during issues. The provided middleware catches potential errors in the API endpoint, logs them using Winston, and presents user-friendly messages without revealing sensitive information.
How to structure a Node.js project for SMS reminders?
The guide recommends a structure including directories for configuration (`config`), database models (`models`), API routes (`routes`), controllers (`controllers`), and middleware (`middleware`). This separation of concerns makes the application more maintainable and scalable.
What are the prerequisites for this SMS reminder project?
You'll need Node.js and npm, a MessageBird account with an API key, access to a MongoDB instance, basic understanding of JavaScript and APIs, and a text editor or IDE. It is recommended to use LTS versions of Node.js and the latest stable libraries.
How to handle MessageBird API failures?
The code includes basic error handling for MessageBird API calls, but more robust error handling and retry mechanisms can be added for production. For example, exponential backoff for temporary failures can make the application more resilient. The application is structured to prevent the saving of appointment details if scheduling the SMS fails via MessageBird's API.
What is Winston used for in this project?
Winston is a logging library that helps track events and errors in the application, aiding in debugging and monitoring. It's configured to log to different files for errors and general logs, making it easier to diagnose issues or track usage.
How to schedule an SMS reminder a specific time before an appointment?
Use the Moment Timezone library to accurately calculate reminder times based on the appointment time and the desired time zone. The code demonstrates subtracting the `REMINDER_HOURS_BEFORE` (defined in your .env file) from the appointment time to schedule the SMS.
How to use dotenv for environment variables?
The `dotenv` package is used to load environment variables from a `.env` file. This is essential for securely managing sensitive data like API keys and database credentials, keeping them separate from your codebase.