code examples
code examples
Build a NestJS SMS Scheduling System with Sinch
A guide on creating an SMS scheduling and reminder application using NestJS, Sinch SMS API, PostgreSQL, and Prisma.
This guide provides a complete walkthrough for building a robust SMS scheduling and reminder application using the NestJS framework and the Sinch SMS API. We'll cover everything from initial project setup to deployment and monitoring, enabling you to reliably send timely SMS notifications based on scheduled events.
This application solves the common need for automated reminders – think appointment confirmations, subscription renewals, task deadlines, or event notifications. By leveraging NestJS's structure and Sinch's reliable SMS delivery and scheduling features, we create a scalable and maintainable solution. We will use PostgreSQL with Prisma for persistence.
Project Overview and Goals
What We're Building:
A NestJS backend application that exposes an API to:
- Schedule an SMS reminder for a future date and time.
- Store appointment/reminder details persistently.
- Automatically send the SMS via Sinch at the specified time.
- (Optional) Allow cancelling scheduled reminders.
Technologies Used:
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Chosen for its modular architecture, built-in features (like validation pipes), and TypeScript support.
- Sinch SMS API: A powerful API for sending and receiving SMS messages globally. Chosen for its direct support for scheduled sends (
send_atparameter), eliminating the need for complex local scheduling logic for the actual send time. We'll use the@sinch/sdk-core. - PostgreSQL: A robust open-source relational database. Chosen for its reliability and data integrity features.
- Prisma: A next-generation ORM for Node.js and TypeScript. Chosen for its type safety, auto-generated migrations, and intuitive API.
- Luxon: A library for handling dates and times. Chosen for its immutability and clear API for time zone management.
- Docker & Docker Compose: For containerizing the application and database for consistent development and deployment environments.
System Architecture:
graph LR
Client[Client Application / API Consumer] -->|1. POST /appointments| API(NestJS API);
API -->|2. Validate & Save| DB[(PostgreSQL w/ Prisma)];
API -->|3. Schedule Send| Sinch(Sinch SMS API);
Sinch -->|4. Send SMS at 'send_at' time| UserDevice(User's Mobile Device);
subgraph NestJS Application
API
AppointmentsService(Appointments Service)
SinchService(Sinch Service)
DB
end
%% Interactions
Client --Makes API Request--> API;
API --Persists Data--> DB;
API --Calls SDK Method--> SinchService;
SinchService --Uses 'send_at' Parameter--> Sinch;- A client sends a POST request to the NestJS API endpoint (
/appointments) with reminder details (recipient number, message content, time). - The NestJS API validates the request, saves the appointment details to the PostgreSQL database via Prisma.
- The NestJS API, through a dedicated
SinchService, calls the Sinch SMS API using the@sinch/sdk-core, providing the recipient, message, and the crucialsend_atparameter set to the desired delivery time (converted to UTC). - Sinch internally handles the scheduling and sends the SMS message at the specified
send_attime to the user's device.
Prerequisites:
- Node.js (v18 or later recommended) and pnpm (or npm/yarn, adjust commands accordingly).
- Docker and Docker Compose installed.
- A Sinch account with API credentials (Project ID, Key ID, Key Secret) and a configured Sender Number (or Alphanumeric Sender ID).
- Basic understanding of TypeScript, NestJS concepts, and REST APIs.
- A code editor (like VS Code).
- A tool for making API requests (like Postman or
curl).
1. Setting up the Project
Let's initialize our NestJS project and install the necessary dependencies.
-
Create NestJS Project: Open your terminal and run the NestJS CLI command:
bashnpx @nestjs/cli new sinch-sms-scheduler cd sinch-sms-schedulerChoose
pnpmwhen prompted by the NestJS CLI for the package manager, or adjust the commands below if you prefernpmoryarn. -
Install Dependencies:
bash# Sinch SDK, Date/Time handling, HTTP client (often needed) pnpm add @sinch/sdk-core luxon @nestjs/axios axios # Validation & Configuration pnpm add class-validator class-transformer @nestjs/config dotenv # Scheduling (Built-in) - No explicit install needed # Prisma (ORM & Client) pnpm add @prisma/client pnpm add -D prisma # Install Prisma CLI as a dev dependency # PostgreSQL driver pnpm add pg -
Initialize Prisma: Set up Prisma with PostgreSQL:
bashpnpm prisma init --datasource-provider postgresqlThis creates a
prismadirectory with aschema.prismafile and a.envfile for database credentials. -
Configure Environment Variables: Update the
.envfile created by Prisma. Add your Sinch credentials and other configurations:dotenv# .env # Database (Prisma) # Replace with your preferred DB connection string format if needed # Example for local Docker setup (see Docker Compose section) DATABASE_URL=""postgresql://user:password@localhost:5432/sms_scheduler?schema=public"" # Sinch Credentials - Replace with your actual values SINCH_PROJECT_ID=""YOUR_SINCH_PROJECT_ID"" SINCH_KEY_ID=""YOUR_SINCH_KEY_ID"" SINCH_KEY_SECRET=""YOUR_SINCH_KEY_SECRET"" SINCH_FROM_NUMBER=""+1xxxxxxxxxx"" # Your Sinch registered sender number in E.164 format SINCH_SMS_REGION=""us"" # Or ""eu"", ""au"", ""br"", ""ca"" depending on your account region # Application Port PORT=3000DATABASE_URL: Connection string for your PostgreSQL database. We'll set up a local one using Docker later.SINCH_PROJECT_ID,SINCH_KEY_ID,SINCH_KEY_SECRET: Replace these placeholders. Obtain these from your Sinch Dashboard under API Credentials. Navigate to your Project > Settings > API Credentials.SINCH_FROM_NUMBER: Replace this placeholder. The phone number or Alphanumeric Sender ID registered with Sinch that messages will be sent from. Must be in E.164 format (e.g.,+12125551234). Get this from your Sinch Dashboard > Numbers.SINCH_SMS_REGION: The region your Sinch account operates in (e.g.,us,eu). This ensures the SDK connects to the correct regional endpoint. Check your Sinch Dashboard or documentation if unsure.PORT: The port your NestJS application will run on.
-
Integrate Configuration Module: Load the environment variables into your NestJS application using
@nestjs/config. Updatesrc/app.module.ts:typescript// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { AppController } from './app.controller'; import { AppService } from './app.service'; // We will add other modules here later (Database, Appointments, Sinch) @Module({ imports: [ ConfigModule.forRoot({ // Load .env variables globally isGlobal: true, envFilePath: '.env', }), // ... other modules will be added here ], controllers: [AppController], providers: [AppService], }) export class AppModule {}ConfigModule.forRoot({ isGlobal: true })makes environment variables accessible throughout the application viaConfigService. -
Set up Docker Compose for Database: Create a
docker-compose.ymlfile in the project root for easy local database setup:yaml# docker-compose.yml version: '3.8' services: postgres: image: postgres:15 container_name: sinch-sms-db environment: POSTGRES_USER: user # Match .env user POSTGRES_PASSWORD: password # Match .env password POSTGRES_DB: sms_scheduler # Match .env database name ports: - ""5432:5432"" # Expose port 5432 locally volumes: - postgres_data:/var/lib/postgresql/data restart: always volumes: postgres_data: driver: localStart the database container:
bashdocker-compose up -d -
Project Structure Overview: Your initial structure will look like this:
sinch-sms-scheduler/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts ├── test/ ├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── docker-compose.yml ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── README.md ├── tsconfig.build.json └── tsconfig.jsonWe will create new modules (
database,sinch,appointments) withinsrcas we build the features.
2. Implementing Core Functionality (Sinch Service)
Let's create a dedicated service to handle interactions with the Sinch SMS API.
-
Generate Sinch Module and Service:
bashnest generate module sinch nest generate service sinch --no-spec # --no-spec skips test file generation for nowThis creates
src/sinch/sinch.module.tsandsrc/sinch/sinch.service.ts. -
Implement Sinch Service: Populate
src/sinch/sinch.service.tswith the logic to initialize the Sinch client and send scheduled messages.typescript// src/sinch/sinch.service.ts import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { SinchClient } from '@sinch/sdk-core'; import { DateTime } from 'luxon'; // Use Luxon for date/time @Injectable() export class SinchService implements OnModuleInit { private readonly logger = new Logger(SinchService.name); private sinchClient: SinchClient; constructor(private configService: ConfigService) {} onModuleInit() { // Initialize Sinch Client when the module loads const projectId = this.configService.get<string>('SINCH_PROJECT_ID'); const keyId = this.configService.get<string>('SINCH_KEY_ID'); const keySecret = this.configService.get<string>('SINCH_KEY_SECRET'); const region = this.configService.get<string>('SINCH_SMS_REGION'); if (!projectId || !keyId || !keySecret || !region || projectId === 'YOUR_SINCH_PROJECT_ID') { this.logger.error('Missing or placeholder Sinch API credentials in environment variables. Please update .env'); throw new Error('Sinch API credentials not configured or are placeholders.'); } this.sinchClient = new SinchClient({ projectId, keyId, keySecret, region }); this.logger.log('Sinch Client Initialized'); } /** * Schedules an SMS message using the Sinch API's send_at feature. * @param to Recipient phone number in E.164 format (e.g., +1xxxxxxxxxx) * @param message The SMS message body * @param sendAt The Date object representing when the message should be sent * @returns The batch ID from the Sinch API response * @throws Error if the Sinch API call fails */ async scheduleSms(to: string, message: string, sendAt: Date): Promise<string> { const fromNumber = this.configService.get<string>('SINCH_FROM_NUMBER'); if (!fromNumber || fromNumber === '+1xxxxxxxxxx') { this.logger.error('SINCH_FROM_NUMBER not configured or is a placeholder.'); throw new Error('Sender number not configured or is a placeholder.'); } // --- CRITICAL: Convert sendAt Date to UTC ISO string for Sinch API --- // Luxon helps manage timezones correctly. Ensure the input 'sendAt' // represents the *intended local time* for the reminder. // We convert it to UTC because Sinch 'send_at' expects UTC. const sendAtUtcIso = DateTime.fromJSDate(sendAt).toUTC().toISO(); // Example: If sendAt is 2025-04-20 14:00:00 in America/New_York (EDT, UTC-4), // this converts it to ""2025-04-20T18:00:00.000Z"" this.logger.log(`Scheduling SMS to ${to} from ${fromNumber} at ${sendAtUtcIso}`); try { const response = await this.sinchClient.sms.batches.send({ sendSMSRequestBody: { to: [to], // Must be an array from: fromNumber, body: message, send_at: sendAtUtcIso, // Use the UTC ISO string // Optional: Add delivery_report: 'full' or 'summary' if needed }, }); this.logger.log(`SMS scheduled successfully. Batch ID: ${response.id}`); return response.id; // Return the batch ID for tracking } catch (error) { this.logger.error(`Failed to schedule SMS via Sinch: ${error.message}`, error.stack); // Consider re-throwing a custom error or handling specific Sinch API errors throw new Error(`Sinch API Error: ${error.message}`); } } // Add other methods if needed, e.g., cancelScheduledSms(batchId) // Note: Sinch API might require specific endpoints/methods for cancellation. // Check Sinch documentation for batch cancellation capabilities. }OnModuleInit: Initializes theSinchClientonce when the module is loaded, ensuring credentials are read correctly and are not placeholders.scheduleSms:- Takes the recipient (
to), message (body), and the desired send time (sendAtas a JavaScriptDateobject). - Retrieves configuration using
ConfigService, checking for placeholder values. - Crucially, uses
Luxonto convert thesendAtDateobject into a UTC ISO 8601 string (YYYY-MM-DDTHH:mm:ss.sssZ), which is the format required by the Sinchsend_atparameter. This handles time zone conversions correctly. - Calls
sinchClient.sms.batches.sendwith the required parameters. - Logs success or failure and returns the
batchIdfrom Sinch, which can be used for tracking or potential cancellation.
- Takes the recipient (
- Error Handling: Basic logging is included. Production systems should have more robust error handling (see Section 5).
-
Export and Register Service: Make sure
SinchServiceis exported from its module and theSinchModuleis imported into the mainAppModule.typescript// src/sinch/sinch.module.ts import { Module } from '@nestjs/common'; import { SinchService } from './sinch.service'; // ConfigModule is already global, no need to import here unless specific config scoping is needed @Module({ providers: [SinchService], exports: [SinchService], // Export the service for other modules to use }) export class SinchModule {}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 { SinchModule } from './sinch/sinch.module'; // Import SinchModule import { DatabaseModule } from './database/database.module'; // Will add later import { AppointmentsModule } from './appointments/appointments.module'; // Will add later @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), SinchModule, // Register SinchModule // DatabaseModule, // Register later // AppointmentsModule, // Register later ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
3. Building the API Layer (Appointments)
Now, let's create the API endpoints to receive scheduling requests.
-
Generate Appointments Module, Controller, Service:
bashnest generate module appointments nest generate controller appointments --no-spec nest generate service appointments --no-spec -
Define Data Transfer Object (DTO): Create a DTO to define the expected request body shape and apply validation rules.
typescript// src/appointments/dto/create-appointment.dto.ts import { IsString, IsNotEmpty, IsPhoneNumber, IsDateString, MinDate, IsOptional, IsDate, // Add IsDate for validation after transformation } from 'class-validator'; import { Transform } from 'class-transformer'; export class CreateAppointmentDto { @IsString() @IsNotEmpty() patientName: string; @IsString() @IsOptional() // Example: Doctor name might be optional doctorName?: string; @IsPhoneNumber(null, { message: 'Phone number must be a valid E.164 format (e.g., +12125551234)' }) // Validate E.164 format @IsNotEmpty() phoneNumber: string; @IsDateString({ strict: true }, { message: 'Appointment time must be a valid ISO 8601 date string (e.g., 2025-12-31T14:30:00.000Z)' }) @IsNotEmpty() @Transform(({ value }) => new Date(value), { toClassOnly: true }) // Transform string to Date object *before* validation @IsDate() // Validate that the transformation resulted in a Date object @MinDate(new Date(), { message: 'Appointment time must not be in the past' }) // Use standard MinDate appointmentTime: Date; // Will be received as ISO string, transformed to Date @IsString() @IsNotEmpty() message: string; // The custom message to send }- Uses
class-validatordecorators for robust validation (required fields, phone number format, date format, non-past date). @IsPhoneNumber(null)validates against E.164 format by default.@IsDateStringensures the input is a valid ISO 8601 date string.@Transform(({ value }) => new Date(value), { toClassOnly: true })converts the valid date string into a JavaScriptDateobject before other date validations run.@IsDate()ensures the transformation was successful.@MinDate(new Date())ensures the date is now or in the future (not strictly future, but not past).
- Uses
-
Implement Appointments Controller: Define the API endpoint in
src/appointments/appointments.controller.ts.typescript// src/appointments/appointments.controller.ts import { Controller, Post, Body, Logger, HttpException, HttpStatus, ParseIntPipe, Get, Param } from '@nestjs/common'; import { AppointmentsService } from './appointments.service'; import { CreateAppointmentDto } from './dto/create-appointment.dto'; import { Appointment } from '@prisma/client'; // Import Prisma model later @Controller('appointments') export class AppointmentsController { private readonly logger = new Logger(AppointmentsController.name); constructor(private readonly appointmentsService: AppointmentsService) {} @Post() // @UsePipes(new ValidationPipe(...)) // No longer needed if using global pipe in main.ts async scheduleAppointment( @Body() createAppointmentDto: CreateAppointmentDto, // DTO is validated by global pipe ): Promise<{ message: string; appointment: Appointment }> { // Return the created Appointment this.logger.log(`Received request to schedule appointment: ${JSON.stringify(createAppointmentDto)}`); try { // The DTO is already validated and transformed by the global ValidationPipe const newAppointment = await this.appointmentsService.scheduleAppointment(createAppointmentDto); this.logger.log(`Appointment scheduled successfully with ID: ${newAppointment.id}`); return { message: 'Appointment scheduled successfully and SMS reminder queued.', appointment: newAppointment, // Return the full appointment record }; } catch (error) { this.logger.error(`Failed to schedule appointment: ${error.message}`, error.stack); // Re-throw HTTP exceptions directly, wrap others if (error instanceof HttpException) { throw error; } throw new HttpException('Failed to schedule appointment.', HttpStatus.INTERNAL_SERVER_ERROR); } } @Get(':id') async getAppointment(@Param('id', ParseIntPipe) id: number): Promise<Appointment> { const appointment = await this.appointmentsService.getAppointmentById(id); if (!appointment) { throw new HttpException('Appointment not found', HttpStatus.NOT_FOUND); } return appointment; } // Add other endpoints later if needed (DELETE /:id, etc.) }@Controller('appointments'): Defines the base route/appointments.@Post(): Handles POST requests to/appointments. Assumes globalValidationPipeis enabled inmain.ts.- Injects
AppointmentsServiceto handle the business logic. - Returns a success message and the created
Appointmentobject from the database. - Includes error handling, re-throwing known
HttpExceptions (likeBadRequestExceptionfrom the service) and wrapping others in a generic500 Internal Server Error. - Added a simple
GET /appointments/:idendpoint example.
-
Implement Appointments Service (Initial): Create the business logic in
src/appointments/appointments.service.ts. This service will orchestrate saving to the database (later) and calling theSinchService.typescript// src/appointments/appointments.service.ts import { Injectable, Logger, BadRequestException } from '@nestjs/common'; // Import BadRequestException import { CreateAppointmentDto } from './dto/create-appointment.dto'; import { SinchService } from '../sinch/sinch.service'; // import { DatabaseService } from '../database/database.service'; // Import later import { Appointment } from '@prisma/client'; // Import later import { Duration, DateTime } from 'luxon'; @Injectable() export class AppointmentsService { private readonly logger = new Logger(AppointmentsService.name); constructor( private readonly sinchService: SinchService, // private readonly databaseService: DatabaseService, // Inject later ) {} async scheduleAppointment(dto: CreateAppointmentDto): Promise<any> { // Update return type later to Promise<Appointment> this.logger.log(`Processing appointment scheduling for ${dto.phoneNumber}`); // --- Business Logic Validation (Example: Ensure time is reasonably far in the future) --- // Although DTO validates >= now, business logic might require ""at least 5 mins from now"" const minLeadTime = Duration.fromObject({ minutes: 5 }); if (DateTime.fromJSDate(dto.appointmentTime) < DateTime.now().plus(minLeadTime)) { this.logger.warn(`Appointment time ${dto.appointmentTime.toISOString()} is too soon (less than 5 minutes away).`); // Throw BadRequestException for client-fixable errors throw new BadRequestException('Appointment time must be at least 5 minutes in the future.'); } // --- TODO: Step 1: Save appointment details to the database --- // We will add this in the Database section (Section 6) // const savedAppointment = await this.databaseService.createAppointment(dto); // For now, let's simulate a saved object: const simulatedSavedAppointment = { id: Math.floor(Math.random() * 1000), // Temporary ID ...dto, status: 'PENDING_SAVE', // Temporary status createdAt: new Date(), updatedAt: new Date(), sinchBatchId: null, }; this.logger.log(`Simulated save for appointment ID: ${simulatedSavedAppointment.id}`); // --- Step 2: Schedule the SMS via Sinch --- try { const batchId = await this.sinchService.scheduleSms( dto.phoneNumber, dto.message, // Use the message from the DTO dto.appointmentTime, // Pass the Date object (already transformed) ); // --- TODO: Step 3: Update the appointment record with the Sinch Batch ID and status --- // We will update the actual DB record later // await this.databaseService.updateAppointmentStatus(savedAppointment.id, 'SCHEDULED', batchId); simulatedSavedAppointment.sinchBatchId = batchId; simulatedSavedAppointment.status = 'SCHEDULED'; this.logger.log(`Updated simulated appointment ${simulatedSavedAppointment.id} with Batch ID ${batchId}`); return simulatedSavedAppointment; // Return the (simulated) saved appointment data } catch (error) { this.logger.error(`Failed to schedule SMS via Sinch for appointment ${simulatedSavedAppointment.id}: ${error.message}`); // --- TODO: Step 4: Handle failure - Update appointment status in DB to 'FAILED' --- // await this.databaseService.updateAppointmentStatus(savedAppointment.id, 'FAILED'); simulatedSavedAppointment.status = 'FAILED'; // Re-throw the error to be caught by the controller throw error; // Controller will wrap this in HttpException if it's not already one } } // Add methods for getAppointment, cancelAppointment later async getAppointmentById(id: number): Promise<any | null> { this.logger.log(`Fetching appointment with ID: ${id} (Simulation)`); // Simulate finding an appointment - replace with DB call later if (id < 1000) { // Simulate finding some IDs return { id: id, status: 'SCHEDULED', /* other fields */ }; } return null; } }- Injects
SinchService. - Includes placeholder comments for database interactions.
- Uses
BadRequestExceptionfor the 5-minute lead time validation, allowing the client to potentially fix the request. - Calls
sinchService.scheduleSmswith the details from the validated and transformed DTO. - Handles potential errors from the
SinchServicecall.
- Injects
-
Register Modules: Ensure the
AppointmentsModuleis registered insrc/app.module.tsand that it importsSinchModule.typescript// src/appointments/appointments.module.ts import { Module } from '@nestjs/common'; import { AppointmentsService } from './appointments.service'; import { AppointmentsController } from './appointments.controller'; import { SinchModule } from '../sinch/sinch.module'; // Import SinchModule // import { DatabaseModule } from '../database/database.module'; // Import later @Module({ imports: [ SinchModule, // Make SinchService available for injection // DatabaseModule, // Import later ], controllers: [AppointmentsController], providers: [AppointmentsService], }) export class AppointmentsModule {}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 { SinchModule } from './sinch/sinch.module'; import { AppointmentsModule } from './appointments/appointments.module'; // Import AppointmentsModule import { DatabaseModule } from './database/database.module'; // Import later @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }), SinchModule, AppointmentsModule, // Register AppointmentsModule // DatabaseModule, // Register later ], controllers: [AppController], providers: [AppService], }) export class AppModule {} -
Enable Validation Globally: It's cleaner to enable the
ValidationPipeglobally insrc/main.ts.typescript// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; // Import Logger import { ConfigService } from '@nestjs/config'; // Import ConfigService async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); // Get ConfigService instance const port = configService.get<number>('PORT', 3000); // Get port from env, default 3000 const logger = new Logger('Bootstrap'); // Create a logger instance app.useGlobalPipes( new ValidationPipe({ transform: true, // Automatically transform payloads to DTO instances whitelist: true, // Strip properties not defined in DTO forbidNonWhitelisted: true, // Throw error if extra properties are sent transformOptions: { enableImplicitConversion: false, // Be explicit about conversions using @Transform }, // Ensure validation errors aren't too verbose in production // disableErrorMessages: process.env.NODE_ENV === 'production', }), ); // Optional: Add global prefix // app.setGlobalPrefix('api/v1'); await app.listen(port); logger.log(`Application listening on port ${port}`); logger.log(`API endpoint available at http://localhost:${port}`); // Adjust if using global prefix } bootstrap();The
@UsePipes(...)decorator can now be removed from the controller method.
4. Integrating with Sinch (Covered in Section 2)
Section 2 detailed the creation of SinchService which encapsulates all interaction with the Sinch SDK. Key points for integration:
- Credentials: Securely loaded from
.envviaConfigService. Ensure placeholders are replaced with real values. - Initialization:
SinchClientis initialized inonModuleInit. - Scheduling: The
scheduleSmsmethod handles the API call, including the criticalsend_atparameter formatted as a UTC ISO string. - Error Handling: Basic error logging is in place.
- Configuration:
SINCH_PROJECT_ID,SINCH_KEY_ID,SINCH_KEY_SECRET,SINCH_FROM_NUMBER,SINCH_SMS_REGIONenvironment variables are essential. Double-check these values in your.envfile and ensure they match your Sinch dashboard details precisely.
Obtaining Credentials:
- Log in to your Sinch account (https://dashboard.sinch.com/).
- Navigate to the relevant Project (or create one).
- Go to Settings -> API Credentials.
- Copy the Project ID, Key ID, and Key Secret. Treat the Key Secret like a password – do not commit it to version control. Place these in your
.envfile. - Go to Numbers -> Your Numbers.
- Copy the desired Sender Number in E.164 format (e.g.,
+1xxxxxxxxxx). Ensure this number is SMS enabled. Place this inSINCH_FROM_NUMBERin your.envfile. - Note your Account Region (usually visible in the dashboard URL or account settings) for the
SINCH_SMS_REGIONvariable in.env.
5. Implementing Error Handling, Logging, and Retry Mechanisms
Production systems require more robust error handling.
-
Consistent Error Strategy (Exception Filters): NestJS uses Exception Filters. The global
ValidationPipehandles DTO errors (400). The controller catches service errors, re-throwsHttpExceptions (likeBadRequestException), and wraps others as 500 errors. This provides a good baseline.Refined Service Error Handling Example:
typescript// src/sinch/sinch.service.ts // ... inside scheduleSms try-catch block ... catch (error) { this.logger.error(`Failed to schedule SMS via Sinch: ${error.message}`, error.stack); // You could check for specific Sinch error types/codes if the SDK provides them // and throw more specific custom exceptions if needed. // Example: if (error?.response?.data?.code === 'SOME_SINCH_CODE') { ... } throw new Error(`Sinch API Error during scheduling: ${error.message}`); // Re-throw generic error } // src/appointments/appointments.service.ts // ... inside scheduleAppointment try-catch block for Sinch call ... // (Error handling already present, re-throws the error from SinchService)
Frequently Asked Questions
How to schedule SMS reminders with NestJS?
Use NestJS with the Sinch SMS API to build an SMS scheduling application. Create a NestJS backend that interacts with the Sinch API to send SMS messages at specified times, storing reminder details in a database like PostgreSQL.
What is the Sinch SMS API used for in NestJS?
The Sinch SMS API enables sending and receiving SMS messages globally within a NestJS application. Its `send_at` parameter allows direct scheduling of messages, simplifying the application logic.
Why use Prisma with PostgreSQL in this project?
Prisma, an ORM for Node.js and TypeScript, enhances type safety and simplifies database interactions. It's used with PostgreSQL for reliable data persistence and integrity in the SMS scheduling application.
When should I convert sendAt time to UTC for Sinch?
Always convert the desired send time (`sendAt`) to UTC using a library like Luxon before sending it to the Sinch API. Sinch's `send_at` parameter expects a UTC ISO 8601 string for accurate scheduling.
Can I cancel scheduled reminders with the Sinch API?
The Sinch API has batch cancellation capabilities, so cancelling scheduled SMS messages may be possible. Check Sinch's documentation for how to use the necessary endpoints and methods for cancellation.
How to set up a NestJS project for SMS scheduling?
Use the NestJS CLI to create a new project, then install dependencies like `@sinch/sdk-core`, `luxon`, `@nestjs/axios`, and Prisma. Configure environment variables for Sinch and database credentials.
What is Luxon used for in SMS scheduling?
Luxon is a date and time handling library used for its immutable nature and clear API for time zone management. This ensures accurate conversion of scheduled times to UTC for the Sinch API.
How to handle Sinch API credentials securely in NestJS?
Store Sinch API credentials (Project ID, Key ID, Key Secret) in a `.env` file. Load these environment variables into your NestJS application using the `@nestjs/config` module, ensuring they are not committed to version control.
What is the purpose of the SinchService in this NestJS project?
SinchService encapsulates interactions with the Sinch SMS API, initializing the Sinch client and providing methods to schedule SMS messages using credentials and the send_at parameter.
How to validate appointment scheduling requests in NestJS?
Use class-validator and class-transformer to define a Data Transfer Object (DTO) with validation rules. Enable a global ValidationPipe in `main.ts` to automatically validate incoming requests against the DTO schema.
What database is used for storing appointment details?
PostgreSQL is used for persistent storage of appointment details like patient name, phone number, appointment time, and the SMS message content.
How to handle errors when scheduling SMS messages with Sinch?
Implement error handling within the SinchService and AppointmentsService. Catch potential errors from the Sinch API call, log them for debugging, and update the appointment status in the database accordingly.
Why use Docker Compose for the PostgreSQL database?
Docker Compose simplifies local database setup, providing a consistent environment. The provided `docker-compose.yml` file sets up a PostgreSQL container with defined credentials and port mappings.
Where can I find my Sinch API credentials?
Log in to your Sinch account dashboard, navigate to your project, and go to Settings -> API Credentials. There you'll find your Project ID, Key ID, and Key Secret.