code examples
code examples
MessageBird SMS Scheduling with Node.js and NestJS: Complete Guide
Build production-ready SMS appointment reminders using MessageBird API, Node.js, NestJS, and PostgreSQL. Learn scheduling, validation, and deployment.
This guide walks you through creating a production-ready SMS appointment scheduling and reminder system using NestJS, Node.js, and the MessageBird API. You'll learn everything from initial project setup to deployment and monitoring.
Important Note (2024): MessageBird rebranded as "Bird" in February 2024. The existing MessageBird APIs and SDKs remain fully functional and compatible with the new branding (Bird Legal Change Notice, February 2024). This guide uses the messagebird npm package (v4.0.1, published January 25, 2023), which continues to work with Bird's infrastructure. Find new API documentation at bird.com/api-reference, while legacy documentation remains at developers.messagebird.com.
Project Overview and Goals
Build a backend system that enables users (or other systems) to schedule appointments and automatically trigger SMS reminders via MessageBird at a set time before each appointment.
Problem Solved: Reduce no-shows for appointments by sending timely SMS reminders, improving operational efficiency and customer experience. Automate the reminder process, eliminating manual effort.
Technologies:
- Node.js: Runtime environment for your backend application. Use Node.js v20 LTS (maintenance support until April 30, 2026) or v22 LTS (entered LTS October 29, 2024, active support until October 21, 2025, maintenance until April 30, 2027) per the official Node.js release schedule.
- NestJS: Progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Its modular architecture, dependency injection, and TypeScript support make it ideal for robust applications. Compatible with NestJS v10.x and v11.x (v11.0.0 released January 20, 2025).
- MessageBird (now Bird): Communications platform for sending SMS messages and validating phone numbers. The MessageBird Node.js SDK (v4.0.1) remains fully functional following the 2024 rebrand to Bird.
- TypeScript: Enhances JavaScript with static typing for better code quality and maintainability.
- PostgreSQL (Optional but Recommended): Powerful open-source relational database for persisting appointment data. Uses TypeORM as the Object-Relational Mapper (ORM).
- Docker: Containerizes the application for consistent development and deployment environments.
System Flow:
- A client sends a request to the NestJS API endpoint to schedule an appointment.
- The Controller receives the request, validating input using Data Transfer Objects (DTOs).
- The Controller calls the
AppointmentServiceto handle the business logic. - The
AppointmentServicevalidates the phone number using MessageBird Lookup. - The
AppointmentService(optionally) persists appointment details to the database (e.g., PostgreSQL via TypeORM). - The
AppointmentServiceuses the MessageBird SDK to schedule the SMS reminder via the MessageBird API. - MessageBird sends the SMS at the scheduled time.
Prerequisites:
- Node.js LTS version (v20.x or v22.x recommended) and npm/yarn installed. Note: Node.js v18 reached End-of-Life on April 30, 2025 – upgrade to v20 or v22.
- A MessageBird account with API credentials (API Key). Following the 2024 rebrand to Bird, obtain API keys from the Bird Dashboard (formerly MessageBird Dashboard).
- Basic understanding of TypeScript and REST APIs.
- Access to a terminal or command prompt.
- (Optional) Docker and Docker Compose installed.
- (Optional) PostgreSQL database running locally or accessible.
Final Outcome: A robust NestJS application with an API endpoint to schedule appointments, validate phone numbers, store appointment data, and reliably schedule SMS reminders via MessageBird.
1. Setting up the Project
Initialize your NestJS project and install the necessary dependencies.
-
Install NestJS CLI: If you don't have the NestJS CLI installed globally, run:
bashnpm install -g @nestjs/cli -
Create New NestJS Project:
bashnest new nestjs-messagebird-reminders cd nestjs-messagebird-remindersChoose your preferred package manager (npm or yarn) when prompted.
-
Install Dependencies: Install modules for configuration, validation, MessageBird integration, and database operations (if using one).
bash# Core dependencies npm install @nestjs/config class-validator class-transformer dotenv # MessageBird SDK (v4.0.1 published January 2023, remains compatible) npm install messagebird # Date handling npm install date-fns # Database (Optional: PostgreSQL + TypeORM) npm install @nestjs/typeorm typeorm pg # Optional: For complex internal application scheduling tasks (background jobs, not SMS scheduling) # npm install @nestjs/schedule@nestjs/config: Manages environment variables.class-validator,class-transformer: Validate request DTOs.dotenv: Loads environment variables from a.envfile.messagebird: Official Node.js SDK for the MessageBird API (v4.0.1, January 25, 2023). Despite being published in 2023, it remains fully functional with Bird's current infrastructure.date-fns: Modern library for date manipulation.@nestjs/typeorm,typeorm,pg: PostgreSQL integration via TypeORM.@nestjs/schedule: Useful for internal application scheduling, distinct from MessageBird's SMS scheduling.
-
Environment Configuration (
.env): Create a.envfile in your project root:dotenv# .env PORT=3000 # MessageBird Credentials & Settings (Bird as of Feb 2024) MESSAGEBIRD_API_KEY=YOUR_LIVE_OR_TEST_API_KEY MESSAGEBIRD_ORIGINATOR=YourAppName # Or your MessageBird phone number MESSAGEBIRD_LOOKUP_COUNTRY_CODE=US # Default country code for Lookup API (ISO 3166-1 alpha-2) REMINDER_HOURS_BEFORE=3 # Hours before appointment to send reminder # Database (Optional – Example for PostgreSQL) DB_HOST=localhost DB_PORT=5432 DB_USERNAME=postgres DB_PASSWORD=your_db_password DB_DATABASE=reminders NODE_ENV=development # Set to 'production' in production environmentsMESSAGEBIRD_API_KEY: Obtain this from your Bird Dashboard (formerly MessageBird Dashboard: Developers → API access → Show key). Use a live key for real messages or a test key for development without sending actual SMS. API keys created before the February 2024 rebrand remain valid.MESSAGEBIRD_ORIGINATOR: The sender ID displayed on the recipient's phone. Use an alphanumeric string (max 11 chars, country restrictions apply) or a purchased MessageBird/Bird virtual mobile number (required for some countries like the US). Check Bird documentation for country-specific restrictions.MESSAGEBIRD_LOOKUP_COUNTRY_CODE: Helps the Lookup API parse numbers entered without a country code (use ISO 3166-1 alpha-2 format).REMINDER_HOURS_BEFORE: Configures the reminder timing.- Database Variables: Adjust these if you're using a database.
NODE_ENV: Important for TypeORM configuration (see Section 6).
-
Load Configuration (
app.module.ts): Configure theConfigModuleto load the.envfile.typescript// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AppointmentModule } from './appointment/appointment.module'; // Import TypeOrmModule if using a database (Section 6) // import { TypeOrmModule } from '@nestjs/typeorm'; // Import HealthModule if using health checks (Section 10) // import { HealthModule } from './health/health.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Make config available globally envFilePath: '.env', }), // Add TypeOrmModule configuration here if using a database (Section 6) AppointmentModule, // We will create this module next // Add HealthModule here if implementing health checks (Section 10) ], controllers: [AppController], providers: [AppService], }) export class AppModule {} -
Enable Validation Pipe (
main.ts): Enable the global validation pipe to automatically validate incoming request bodies based on DTOs.typescript// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); const port = configService.get<number>('PORT') || 3000; app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not defined in DTO transform: true, // Automatically transform payloads to DTO instances forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are present })); await app.listen(port); console.log(`Application is running on: ${await app.getUrl()}`); } bootstrap(); -
Project Structure: NestJS encourages a modular structure. We'll create an
AppointmentModuleto encapsulate all appointment-related logic.bashnest g module appointment nest g controller appointment --no-spec # Skip spec files for brevity nest g service appointment --no-specThis creates:
src/appointment/appointment.module.tssrc/appointment/appointment.controller.tssrc/appointment/appointment.service.ts
Create a DTO directory:
mkdir src/appointment/dto
2. Implementing Core Functionality (Scheduling Logic)
Implement the service responsible for validating data, interacting with MessageBird, and (optionally) saving to the database.
-
Define Appointment DTO (
create-appointment.dto.ts): Create a Data Transfer Object to define the expected shape and validation rules for incoming request bodies.typescript// src/appointment/dto/create-appointment.dto.ts import { IsString, IsNotEmpty, IsPhoneNumber, IsDateString, MinLength, IsISO8601, } from 'class-validator'; export class CreateAppointmentDto { @IsString() @IsNotEmpty() @MinLength(2) customerName: string; // Basic validation via @IsPhoneNumber. // WARNING: For robust production validation (strict E.164, international formats), // this might be insufficient. Strongly consider using 'google-libphonenumber' // wrapped in a custom NestJS validator decorator. @IsPhoneNumber(null) // Pass region code (e.g., 'US') if needed for non-E.164 numbers @IsNotEmpty() phoneNumber: string; // Expect E.164 format ideally (e.g., +14155552671) @IsString() @IsNotEmpty() treatment: string; @IsISO8601({ strict: true }, { message: 'appointmentTime must be a valid ISO 8601 date string (e.g., 2025-12-31T14:30:00.000Z)'}) @IsNotEmpty() appointmentTime: string; // Expect ISO 8601 format UTC (e.g., '2025-12-31T14:30:00.000Z') }- We use
class-validatordecorators to enforce rules. @IsPhoneNumber: Provides basic validation. See warning in the code comment about production readiness.@IsISO8601: Ensures the date uses the correct format. Using UTC is crucial for avoiding timezone issues.
- We use
-
Implement
AppointmentService: This service will contain the core logic. Note: This version does not include database persistence (see Section 6 for the optional DB integration).typescript// src/appointment/appointment.service.ts import { Injectable, Logger, BadRequestException, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as MessageBird from 'messagebird'; // Import MessageBird SDK import { CreateAppointmentDto } from './dto/create-appointment.dto'; import { subHours, isBefore, parseISO, formatISO } from 'date-fns'; // Modern date library // Define types for MessageBird client and responses (optional but good practice) type MessageBirdClient = ReturnType<typeof MessageBird>; // Define interfaces based on actual MessageBird SDK response structures if needed @Injectable() export class AppointmentService { private readonly logger = new Logger(AppointmentService.name); private messagebird: MessageBirdClient; private readonly apiKey: string; private readonly originator: string; private readonly countryCode: string; private readonly reminderHoursBefore: number; constructor(private configService: ConfigService) { this.apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY'); this.originator = this.configService.get<string>('MESSAGEBIRD_ORIGINATOR'); this.countryCode = this.configService.get<string>('MESSAGEBIRD_LOOKUP_COUNTRY_CODE'); // Read reminder hours from config, default to 3 if not set this.reminderHoursBefore = parseInt(this.configService.get<string>('REMINDER_HOURS_BEFORE', '3'), 10); if (!this.apiKey || !this.originator) { this.logger.error('MessageBird API Key or Originator not configured!'); throw new InternalServerErrorException('Messaging service configuration error.'); } if (isNaN(this.reminderHoursBefore) || this.reminderHoursBefore <= 0) { this.logger.warn(`Invalid REMINDER_HOURS_BEFORE value. Defaulting to 3.`); this.reminderHoursBefore = 3; } // Initialize MessageBird client this.messagebird = MessageBird(this.apiKey); } /** * Schedules an SMS reminder for an appointment. * This version does NOT persist to a database. See Section 6 for DB integration. */ async scheduleAppointmentReminder(dto: CreateAppointmentDto): Promise<{ id: string; message: string }> { this.logger.log(`Scheduling reminder for ${dto.customerName} at ${dto.appointmentTime}`); const appointmentDateTime = parseISO(dto.appointmentTime); const reminderDateTime = subHours(appointmentDateTime, this.reminderHoursBefore); const now = new Date(); // 1. Validate Appointment Time const minReminderTime = new Date(now.getTime() + 5 * 60000); // 5 minutes from now buffer if (isBefore(reminderDateTime, minReminderTime)) { this.logger.warn(`Failed scheduling: Reminder time ${formatISO(reminderDateTime)} is too soon.`); throw new BadRequestException(`Appointment must be scheduled far enough in advance to allow for the ${this.reminderHoursBefore}-hour reminder (at least 5 minutes from now).`); } // 2. Validate Phone Number via MessageBird Lookup let validatedPhoneNumber: string; try { validatedPhoneNumber = await new Promise<string>((resolve, reject) => { this.messagebird.lookup.read(dto.phoneNumber, this.countryCode, (err: any, response: any) => { if (err) { this.logger.error(`Lookup failed for partial number: ${err.errors?.[0]?.description || err.message}`, err.stack); if (err.errors?.[0]?.code === 21) { return reject(new BadRequestException('Invalid phone number format provided.')); } return reject(new InternalServerErrorException('Failed to validate phone number.')); } if (response.type !== 'mobile') { this.logger.warn(`Lookup successful for partial number, but type is '${response.type}'.`); return reject(new BadRequestException('The provided phone number must be a mobile number.')); } this.logger.log(`Lookup successful. Normalized number format obtained.`); resolve(response.phoneNumber); // Use the normalized number }); }); } catch (error) { throw error; // Re-throw exceptions from the promise reject calls } // 3. Schedule the SMS with MessageBird const reminderBody = `${dto.customerName}, here's a reminder for your ${dto.treatment} appointment scheduled for ${formatISO(appointmentDateTime)}. See you soon!`; const params = { originator: this.originator, recipients: [validatedPhoneNumber], scheduledDatetime: formatISO(reminderDateTime), // Use ISO 8601 format body: reminderBody, }; try { const messageResponse = await new Promise<any>((resolve, reject) => { this.messagebird.messages.create(params, (err: any, response: any) => { if (err) { this.logger.error(`Failed to schedule SMS: ${err.errors?.[0]?.description || err.message}`, err.stack); return reject(new InternalServerErrorException('Failed to schedule SMS reminder.')); } this.logger.log(`SMS scheduled successfully. Message ID: ${response.id}`); resolve(response); }); }); // 4. (Optional) Persist Appointment to Database (See Section 6) // If implementing Section 6, the database save logic goes here. // 5. Return Success Response (without appointmentId for non-DB version) return { id: messageResponse.id, // MessageBird message ID message: `Appointment reminder scheduled successfully for ${dto.customerName}.`, }; } catch (error) { this.logger.error(`Error during SMS scheduling: ${error.message}`, error.stack); throw new InternalServerErrorException('An error occurred while scheduling the appointment reminder.'); } } }- Initialization: Reads config (including
REMINDER_HOURS_BEFORE) and initializes MessageBird. - Date Handling: Uses
date-fnsfor parsing, calculation (subHours), and validation (isBefore). - Lookup: Calls
messagebird.lookup.readwrapped in aPromise, handles errors, checks formobiletype. - Scheduling: Calls
messagebird.messages.createwithscheduledDatetime, wrapped in aPromise. - Return Type: Returns
Promise<{ id: string; message: string }>as database persistence is not included here. - Error Handling: Uses NestJS exceptions and
Logger.
- Initialization: Reads config (including
3. Building the API Layer
Create the controller to expose an endpoint for scheduling appointments.
// src/appointment/appointment.controller.ts
import { Controller, Post, Body, HttpCode, HttpStatus, Logger } from '@nestjs/common';
import { AppointmentService } from './appointment.service';
import { CreateAppointmentDto } from './dto/create-appointment.dto';
@Controller('appointments')
export class AppointmentController {
private readonly logger = new Logger(AppointmentController.name);
constructor(private readonly appointmentService: AppointmentService) {}
@Post('/schedule')
@HttpCode(HttpStatus.CREATED)
async scheduleReminder(@Body() createAppointmentDto: CreateAppointmentDto) {
this.logger.log(`Received request to schedule reminder for ${createAppointmentDto.customerName}`);
// ValidationPipe in main.ts handles DTO validation
// The return type here will match the AppointmentService return type
// (either with or without appointmentId depending on DB integration)
return this.appointmentService.scheduleAppointmentReminder(createAppointmentDto);
}
}- Defines a POST endpoint at
/appointments/schedule. - Injects the validated
CreateAppointmentDtousing@Body(). - Sets the success status code to 201 Created.
- Delegates the core logic to the injected
AppointmentService.
Testing the Endpoint (Example using curl):
Assuming your app runs on port 3000:
curl -X POST http://localhost:3000/appointments/schedule \
-H "Content-Type: application/json" \
-d '{
"customerName": "Jane Doe",
"phoneNumber": "+12025550154",
"treatment": "Consultation",
"appointmentTime": "2025-12-25T15:00:00.000Z"
}'Note: Use a real mobile number for testing with live keys. The appointmentTime must be later than the configured reminder hours plus a small buffer (e.g., 5 minutes) from the current time, specified in UTC.
Expected Success Response (201 Created – without DB):
{
"id": "mb_message_id_string",
"message": "Appointment reminder scheduled successfully for Jane Doe."
}Expected Error Response (400 Bad Request – e.g., invalid date):
{
"statusCode": 400,
"message": [
"appointmentTime must be a valid ISO 8601 date string (e.g., 2025-12-31T14:30:00.000Z)"
],
"error": "Bad Request"
}4. Integrating with MessageBird (Deep Dive)
You've already initialized the client and used the Lookup and Messages APIs. Review key integration points:
- SDK Initialization: Done in
AppointmentServiceconstructor usingMessageBird(this.apiKey). - API Key Security: Handled via
.envfile andConfigModule. Never commit your API key to version control. Use environment variables in deployment environments. - Dashboard Configuration:
- API Key: Navigate to Bird Dashboard → Developers → API access (REST). Create a new key (choose "Live" or "Test") or copy an existing one. Store it securely.
- Required Permissions: Ensure the API key has permissions for "Lookup API access" and "Send SMS via API" (or similar wording in the dashboard).
- Originator: Consider purchasing a Virtual Mobile Number (VMN) from MessageBird (Numbers section) for better deliverability, especially in countries like the US/Canada that restrict alphanumeric senders. Configure this number or your chosen alphanumeric ID in
MESSAGEBIRD_ORIGINATOR. - Lookup: The
MESSAGEBIRD_LOOKUP_COUNTRY_CODEin.envaids parsing numbers without a country code. - Scheduled Messages: View scheduled (and sent) messages in the Bird Dashboard → SMS → Scheduled overview.
- API Key: Navigate to Bird Dashboard → Developers → API access (REST). Create a new key (choose "Live" or "Test") or copy an existing one. Store it securely.
- Environment Variables: (Defined in Section 1)
MESSAGEBIRD_API_KEY: Your secret key.MESSAGEBIRD_ORIGINATOR: Sender ID.MESSAGEBIRD_LOOKUP_COUNTRY_CODE: Default country for Lookup.REMINDER_HOURS_BEFORE: Reminder timing configuration.
- Fallback Mechanisms: The current implementation relies heavily on MessageBird. For critical systems, consider:
- Retries: Implement retries with exponential backoff for transient network errors (see Section 5).
- Monitoring & Alerting: Set up alerts for high failure rates (see Section 10).
- Secondary Provider (Advanced): Integrate a second SMS provider as a fallback (adds complexity).
5. Error Handling, Logging, and Retry Mechanisms
- Error Handling Strategy:
- Use standard NestJS HTTP exceptions (
BadRequestException,InternalServerErrorException, etc.) thrown from the service layer. - Validate input rigorously using DTOs and
ValidationPipe. - Catch specific MessageBird SDK errors (e.g., code 21 for invalid number) and map them appropriately.
- Log detailed errors server-side.
- Use standard NestJS HTTP exceptions (
- Logging:
- Use the built-in
Loggerservice (@nestjs/common). - Log key events: request received, validation results, MessageBird API calls (start, success, failure), database operations (if used), final outcome.
- Include contextual information (e.g., customer name initial, appointment time).
- Security: Be extremely cautious about logging sensitive data. Do not log full phone numbers or API keys unless absolutely necessary and measures like masking or encryption are in place. Log partial identifiers or correlation IDs instead.
- Consider structured logging (JSON) for production environments.
- Use the built-in
- Retry Mechanisms:
- Lookup/Scheduling: For transient network issues, wrap the
messagebird.lookup.readandmessagebird.messages.createpromise calls with a retry mechanism. Use libraries likeasync-retryor implement a simple loop with exponential backoff and jitter. Limit retry attempts.typescript// Conceptual example using async-retry (install `npm i async-retry @types/async-retry`) import * as retry from 'async-retry'; // ... inside scheduleAppointmentReminder, wrap the Promise creation for lookup: try { validatedPhoneNumber = await retry(async (bail, attempt) => { this.logger.log(`Lookup attempt ${attempt}...`); return new Promise<string>((resolve, reject) => { this.messagebird.lookup.read(dto.phoneNumber, this.countryCode, (err: any, response: any) => { if (err) { if (err.errors?.[0]?.code === 21) { // Non-retryable validation error bail(new BadRequestException('Invalid phone number format. Check the number and try again.')); return; } // For other errors, reject to trigger retry return reject(err); } if (response.type !== 'mobile') { // Non-retryable validation error bail(new BadRequestException('The phone number must be a mobile number, not a landline.')); return; } resolve(response.phoneNumber); }); }); }, { retries: 3, factor: 2, minTimeout: 1000, onRetry: (error, attempt) => { this.logger.warn(`Lookup attempt ${attempt} failed. Retrying... Error: ${error.message}`); } }); } catch (error) { // Handle final error after retries or non-retryable errors (from bail) this.logger.error(`Lookup failed permanently: ${error.message}`, error.stack); if (error instanceof BadRequestException) throw error; // Propagate client errors throw new InternalServerErrorException('Failed to validate phone number after multiple attempts. Please try again later.'); } // Apply similar retry logic around messagebird.messages.create if needed - Database: TypeORM handles some connection retries. Configure the connection pool robustly.
- Lookup/Scheduling: For transient network issues, wrap the
6. Database Schema and Data Layer (Optional: PostgreSQL + TypeORM)
This section details how to add database persistence using TypeORM and PostgreSQL. If you implement this, you need to modify the AppointmentService and AppModule shown in previous sections.
-
Install Dependencies: (Should be done in Section 1)
bash# npm install @nestjs/typeorm typeorm pg -
Configure
TypeOrmModule(app.module.ts): Modify yoursrc/app.module.tsto includeTypeOrmModule.typescript// src/app.module.ts (Modified for Database) import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; // Import TypeOrmModule import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AppointmentModule } from './appointment/appointment.module'; import { Appointment } from './appointment/entities/appointment.entity'; // Import entity // Import HealthModule if using health checks (Section 10) @Module({ imports: [ ConfigModule.forRoot({ /* ... */ }), TypeOrmModule.forRootAsync({ // Configure TypeORM imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ type: 'postgres', host: configService.get<string>('DB_HOST'), port: configService.get<number>('DB_PORT'), username: configService.get<string>('DB_USERNAME'), password: configService.get<string>('DB_PASSWORD'), database: configService.get<string>('DB_DATABASE'), entities: [Appointment], // Register entity synchronize: configService.get<string>('NODE_ENV') !== 'production', // Dev only! CRITICAL: Use migrations in production // **WARNING**: Never use synchronize: true in production. It can cause data loss. // For production, use migrations: [__dirname + '/../db/migrations/*{.ts,.js}'] // migrations: [__dirname + '/../db/migrations/*{.ts,.js}'], // For production // cli: { migrationsDir: 'src/db/migrations' } // For production }), }), AppointmentModule, // HealthModule (if used) ], controllers: [AppController], providers: [AppService], }) export class AppModule {}- CRITICAL:
synchronize: trueis for development only. In production, always usesynchronize: falseand manage schema changes via TypeORM migrations. Settingsynchronize: truein production can cause data loss as TypeORM automatically syncs schema changes without validation.
- CRITICAL:
-
Define
AppointmentEntity: Create the entity file:mkdir src/appointment/entitiestypescript// src/appointment/entities/appointment.entity.ts import { Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'; export enum AppointmentStatus { SCHEDULED = 'SCHEDULED', SENT = 'SENT', // Status could be updated via MessageBird webhooks FAILED = 'FAILED', // Status could be updated via MessageBird webhooks CANCELLED = 'CANCELLED', } @Entity('appointments') // Maps to 'appointments' table export class Appointment { @PrimaryGeneratedColumn('uuid') id: string; @Column({ length: 100 }) customerName: string; @Index() @Column({ length: 20 }) // E.164 max length ~15, add buffer phoneNumber: string; // Store the validated, normalized number @Column({ length: 100 }) treatment: string; @Index() @Column({ type: 'timestamp with time zone' }) // Use TIMESTAMPTZ for PostgreSQL appointmentTime: Date; @Column({ type: 'timestamp with time zone' }) reminderTime: Date; @Column({ nullable: true, length: 64 }) // Store MessageBird's message ID messagebirdMessageId?: string; @Index() @Column({ type: 'enum', enum: AppointmentStatus, default: AppointmentStatus.SCHEDULED, }) status: AppointmentStatus; @CreateDateColumn({ type: 'timestamp with time zone' }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp with time zone' }) updatedAt: Date; } -
Inject Repository and Update Service:
-
Import
TypeOrmModuleinAppointmentModule:typescript// src/appointment/appointment.module.ts (Modified for Database) import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; // Import import { AppointmentController } from './appointment.controller'; import { AppointmentService } from './appointment.service'; import { Appointment } from './entities/appointment.entity'; // Import entity @Module({ imports: [TypeOrmModule.forFeature([Appointment])], // Make Appointment repository available controllers: [AppointmentController], providers: [AppointmentService], }) export class AppointmentModule {} -
Inject Repository and Modify
AppointmentService: Updatesrc/appointment/appointment.service.tsto inject the repository and save the appointment. ThescheduleAppointmentRemindermethod needs modifications to include database persistence.typescript// src/appointment/appointment.service.ts (Modified for Database) import { Injectable, Logger, BadRequestException, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as MessageBird from 'messagebird'; import { CreateAppointmentDto } from './dto/create-appointment.dto'; import { subHours, isBefore, parseISO, formatISO } from 'date-fns'; import { InjectRepository } from '@nestjs/typeorm'; // Import import { Repository } from 'typeorm'; // Import import { Appointment, AppointmentStatus } from './entities/appointment.entity'; // Import type MessageBirdClient = ReturnType<typeof MessageBird>; @Injectable() export class AppointmentService { private readonly logger = new Logger(AppointmentService.name); private messagebird: MessageBirdClient; private readonly apiKey: string; private readonly originator: string; private readonly countryCode: string; private readonly reminderHoursBefore: number; constructor( private configService: ConfigService, @InjectRepository(Appointment) // Inject Appointment repository private appointmentRepository: Repository<Appointment>, ) { this.apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY'); this.originator = this.configService.get<string>('MESSAGEBIRD_ORIGINATOR'); this.countryCode = this.configService.get<string>('MESSAGEBIRD_LOOKUP_COUNTRY_CODE'); this.reminderHoursBefore = parseInt(this.configService.get<string>('REMINDER_HOURS_BEFORE', '3'), 10); if (!this.apiKey || !this.originator) { this.logger.error('MessageBird API Key or Originator not configured!'); throw new InternalServerErrorException('Messaging service configuration error.'); } if (isNaN(this.reminderHoursBefore) || this.reminderHoursBefore <= 0) { this.logger.warn(`Invalid REMINDER_HOURS_BEFORE value. Defaulting to 3.`); this.reminderHoursBefore = 3; } this.messagebird = MessageBird(this.apiKey); } /** * Schedules an SMS reminder for an appointment AND saves to database. * Returns appointment with database ID. */ async scheduleAppointmentReminder(dto: CreateAppointmentDto): Promise<{ appointmentId: string; messagebirdMessageId: string; message: string }> { this.logger.log(`Scheduling reminder for ${dto.customerName} at ${dto.appointmentTime}`); const appointmentDateTime = parseISO(dto.appointmentTime); const reminderDateTime = subHours(appointmentDateTime, this.reminderHoursBefore); const now = new Date(); // 1. Validate Appointment Time const minReminderTime = new Date(now.getTime() + 5 * 60000); if (isBefore(reminderDateTime, minReminderTime)) { this.logger.warn(`Failed scheduling: Reminder time ${formatISO(reminderDateTime)} is too soon.`); throw new BadRequestException(`Appointment must be scheduled far enough in advance to allow for the ${this.reminderHoursBefore}-hour reminder (at least 5 minutes from now).`); } // 2. Validate Phone Number via MessageBird Lookup let validatedPhoneNumber: string; try { validatedPhoneNumber = await new Promise<string>((resolve, reject) => { this.messagebird.lookup.read(dto.phoneNumber, this.countryCode, (err: any, response: any) => { if (err) { this.logger.error(`Lookup failed: ${err.errors?.[0]?.description || err.message}`, err.stack); if (err.errors?.[0]?.code === 21) { return reject(new BadRequestException('Invalid phone number format provided.')); } return reject(new InternalServerErrorException('Failed to validate phone number.')); } if (response.type !== 'mobile') { this.logger.warn(`Lookup successful, but type is '${response.type}'.`); return reject(new BadRequestException('The provided phone number must be a mobile number.')); } this.logger.log(`Lookup successful. Normalized number format obtained.`); resolve(response.phoneNumber); // Use the normalized number }); }); } catch (error) { throw error; } // 3. Schedule the SMS with MessageBird const reminderBody = `${dto.customerName}, reminder for your ${dto.treatment} appointment at ${formatISO(appointmentDateTime)}. See you soon!`; const params = { originator: this.originator, recipients: [validatedPhoneNumber], scheduledDatetime: formatISO(reminderDateTime), body: reminderBody, }; try { const messageResponse = await new Promise<any>((resolve, reject) => { this.messagebird.messages.create(params, (err: any, response: any) => { if (err) { this.logger.error(`Failed to schedule SMS: ${err.errors?.[0]?.description || err.message}`, err.stack); return reject(new InternalServerErrorException('Failed to schedule SMS reminder.')); } this.logger.log(`SMS scheduled successfully. Message ID: ${response.id}`); resolve(response); }); }); // 4. Persist Appointment to Database const appointment = this.appointmentRepository.create({ customerName: dto.customerName, phoneNumber: validatedPhoneNumber, treatment: dto.treatment, appointmentTime: appointmentDateTime, reminderTime: reminderDateTime, messagebirdMessageId: messageResponse.id, status: AppointmentStatus.SCHEDULED, }); const savedAppointment = await this.appointmentRepository.save(appointment); this.logger.log(`Appointment saved to database. ID: ${savedAppointment.id}`); // 5. Return Success Response with both IDs return { appointmentId: savedAppointment.id, messagebirdMessageId: messageResponse.id, message: `Appointment reminder scheduled successfully for ${dto.customerName}.`, }; } catch (error) { this.logger.error(`Error during SMS scheduling: ${error.message}`, error.stack); throw new InternalServerErrorException('An error occurred while scheduling the appointment reminder.'); } } }
-
This completes the database integration section. The service now persists appointments to PostgreSQL and returns both the database appointment ID and MessageBird message ID.
Frequently Asked Questions
How to schedule SMS reminders with NestJS?
Use the `messagebird.messages.create` method with the `scheduledDatetime` parameter set to your desired reminder time in ISO 8601 format. This, combined with NestJS's scheduling capabilities, allows for automated SMS reminders through the MessageBird API. Ensure your API key has the necessary permissions in the MessageBird dashboard.
What is the role of MessageBird in this setup?
MessageBird is the communication platform used to send SMS messages and validate phone numbers. You'll need a MessageBird account and API key to integrate their services. The provided code examples demonstrate how to interact with the MessageBird Lookup and Messages APIs using their Node.js SDK.
Why use NestJS for SMS scheduling?
NestJS provides a robust and structured framework for building scalable server-side applications in Node.js. Its modular architecture, dependency injection, and TypeScript support contribute to a more maintainable and efficient application for handling SMS scheduling and other backend logic.
When should I validate phone numbers with MessageBird?
Validate phone numbers before scheduling SMS reminders using the MessageBird Lookup API. This ensures the number is valid and correctly formatted, improving the reliability of your reminder system. The example code demonstrates how to perform this validation within the scheduling process.
Can I use a different database with NestJS?
Yes, while the article recommends PostgreSQL and provides setup instructions for it, NestJS supports various databases through TypeORM. You can adapt the `TypeOrmModule` configuration in the `app.module.ts` file to connect to your preferred database.
How to set up MessageBird API key in NestJS?
Create a `.env` file in your project's root directory and store your MessageBird API key as `MESSAGEBIRD_API_KEY`. Then, use NestJS's `ConfigModule` to load this environment variable securely into your application's configuration.
What is the purpose of the MESSAGEBIRD_ORIGINATOR variable?
The `MESSAGEBIRD_ORIGINATOR` environment variable sets the sender ID that recipients will see on their phones. This can be an alphanumeric string or a MessageBird Virtual Mobile Number. Be mindful of country-specific restrictions on sender IDs.
How to handle errors when scheduling SMS with MessageBird?
Implement error handling using try-catch blocks around MessageBird API calls and use NestJS's built-in exception handling mechanisms like `BadRequestException` and `InternalServerErrorException`. Log errors for debugging and monitoring. Consider retry mechanisms with exponential backoff for transient network errors.
What is the recommended Node.js version for this project?
The article recommends using a Long-Term Support (LTS) version of Node.js, such as v18 or v20, to ensure stability and compatibility with the other project dependencies like NestJS and the MessageBird SDK.
How to integrate PostgreSQL with NestJS and TypeORM?
Install the necessary packages (`@nestjs/typeorm`, `typeorm`, `pg`), configure the `TypeOrmModule` in your `app.module.ts` file with your database credentials, define your entities (like the `Appointment` entity), and inject the repository into your service to interact with the database.
What date format should I use for appointmentTime?
Use the ISO 8601 format (e.g., '2025-12-31T14:30:00.000Z') in UTC for the `appointmentTime` to avoid timezone issues. The `@IsISO8601` decorator enforces this format and the code uses the `date-fns` library for parsing and manipulation.
How to structure a NestJS project for SMS reminders?
Organize your project using modules. The article demonstrates creating an `AppointmentModule` to encapsulate the appointment scheduling logic, including the controller, service, and DTOs. This promotes code organization and maintainability.
Why is phone number validation important in SMS scheduling?
Validating phone numbers ensures accurate delivery of SMS reminders. The MessageBird Lookup API helps verify the format and type of phone number, preventing wasted messages and improving system reliability. Robust validation using libraries like 'google-libphonenumber' is highly recommended for production.
What is the purpose of the REMINDER_HOURS_BEFORE environment variable?
The `REMINDER_HOURS_BEFORE` variable defines how many hours before the appointment the SMS reminder should be sent. It is read from the `.env` file and used to calculate the `scheduledDatetime` for the MessageBird API call.