Project Overview and Goals
This guide details how to build a robust service using NestJS to send bulk SMS messages via the Plivo API. We'll cover everything from project setup and core implementation to security, error handling, deployment, and monitoring. The goal is to create a scalable and reliable system capable of handling broadcast messaging efficiently.
You'll learn how to leverage NestJS modules, services, DTOs, configuration management, and integrate with Plivo's Node.js SDK (specifically targeting v4.x for examples like new plivo.Client()
). We'll also touch upon essential production considerations like rate limiting, queuing (for high volume), logging, and secure credential management. By the end, you'll have a functional API endpoint ready for broadcasting messages.
What We're Building
A NestJS application with a dedicated API endpoint that accepts a list of phone numbers and a message body. This service will then interact with the Plivo API to send the specified message to all recipient numbers in a single, efficient bulk request (up to Plivo's limits).
Problem Solved
This service addresses the need to send the same SMS message to multiple recipients simultaneously without making individual API calls for each number. This is common for:
- Marketing campaigns
- Emergency alerts and notifications
- Appointment reminders
- System status updates
Technologies Used
- Node.js: The underlying JavaScript runtime.
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript. Its modular architecture is ideal for this type of service.
- TypeScript: Provides static typing for better code quality and maintainability.
- Plivo &
plivo-node
SDK: The communications platform API used for sending SMS, and its official Node.js helper library (Guide assumes v4.x or later). - dotenv &
@nestjs/config
: For managing environment variables and application configuration securely. class-validator
&class-transformer
: For robust request payload validation.- (Optional but Recommended) Redis & BullMQ: For queuing messages for high-throughput scenarios.
- (Optional) Prisma & PostgreSQL: For managing contact lists or logging message history.
System Architecture
graph LR
A[Client / API Caller] -- HTTP POST Request --> B(NestJS API);
B -- Validate Request --> B;
B -- Send Bulk SMS Request --> C(Plivo Service);
C -- Format Destinations & Call API --> D(Plivo API);
D -- SMS Delivery --> E(Recipient Phones);
D -- API Response --> C;
C -- Return Result/Status --> B;
B -- HTTP Response --> A;
subgraph Optional Enhancements
B -- Enqueue Job --> F(Redis Queue / BullMQ);
G(Queue Worker) -- Dequeue Job --> G;
G -- Send via Plivo Service --> C;
B -- Log Request/Response --> H(Logging Service);
C -- Log Plivo Interaction --> H;
B -- Store Message Log --> I(Database / Prisma);
G -- Update Message Log --> I;
end
Prerequisites
- Node.js (LTS version recommended, e.g., v18 or v20) and npm/yarn.
- A Plivo account (Sign up at Plivo.com).
- A Plivo phone number capable of sending SMS (purchased or verified).
- Your Plivo Auth ID and Auth Token.
- Basic understanding of TypeScript, REST APIs, and NestJS concepts.
- (Optional) Docker, PostgreSQL, Redis installed if implementing optional components.
1. Setting up the Project
Let's initialize our NestJS project and install necessary dependencies.
1.1 Install NestJS CLI
If you don't have the NestJS CLI installed globally, run:
npm install -g @nestjs/cli
# or
# yarn global add @nestjs/cli
1.2 Create New NestJS Project
nest new plivo-bulk-sms
cd plivo-bulk-sms
Choose your preferred package manager (npm or yarn) when prompted. This creates a standard NestJS project structure.
1.3 Install Core Dependencies
We need packages for configuration management, Plivo integration, and request validation.
npm install @nestjs/config dotenv plivo-node class-validator class-transformer
# or
# yarn add @nestjs/config dotenv plivo-node class-validator class-transformer
@nestjs/config
: Handles environment variables and configuration loading.dotenv
: Loads environment variables from a.env
file intoprocess.env
.plivo-node
: The official Plivo Node.js SDK (ensure you install a version compatible with the examples, e.g., v4.x or later).class-validator
,class-transformer
: Used for validating incoming request data via DTOs.
.env
)
1.4 Environment Setup (Create a .env
file in the project root directory. Crucially, replace the placeholder values with your actual credentials and number. Never commit this file to version control.
# .env
# Plivo Credentials - REPLACE WITH YOUR ACTUAL VALUES
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
# Default Plivo Source Number (Must be a Plivo number you own/verified)
# Use E.164 format, e.g., +14155552671 - REPLACE WITH YOUR NUMBER
PLIVO_SOURCE_NUMBER=+1XXXXXXXXXX
# Application Port (Optional, defaults to 3000)
PORT=3000
- How to get Plivo Credentials:
- Log in to your Plivo Console.
- Navigate to the
Account
section in the top right menu. - Your
Auth ID
andAuth Token
are displayed prominently. - Purchase or verify a phone number under
Phone Numbers
->Buy Numbers
orVerified Numbers
. Use this as yourPLIVO_SOURCE_NUMBER
.
ConfigModule
1.5 Configure Import and configure the ConfigModule
in your main application module (src/app.module.ts
) to make environment variables available throughout the application via the ConfigService
.
// 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 { PlivoModule } from './plivo/plivo.module';
import { MessagingModule } from './messaging/messaging.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Makes ConfigModule available globally
envFilePath: '.env', // Specify the env file path
}),
PlivoModule, // We will create and import these modules
MessagingModule // in the following steps
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
isGlobal: true
: Avoids needing to importConfigModule
into every other module.envFilePath: '.env'
: Tells the module where to find the environment file.
1.6 Enable Validation Pipe Globally
To automatically validate incoming request bodies using our DTOs, enable the ValidationPipe
globally in src/main.ts
.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, Logger } 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); // Default to 3000 if not set
// Enable global validation pipe
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 extra properties are present
}));
await app.listen(port);
Logger.log(`Application running on: http://localhost:${port}`, 'Bootstrap');
}
bootstrap();
Now our basic project structure and configuration are ready.
2. Implementing Core Functionality (Plivo Service)
We'll create a dedicated module and service to handle all interactions with the Plivo API.
2.1 Generate Plivo Module and Service
Use the NestJS CLI to generate the necessary files.
nest generate module plivo
nest generate service plivo
This creates src/plivo/plivo.module.ts
and src/plivo/plivo.service.ts
.
PlivoService
2.2 Implement This service will encapsulate the Plivo client initialization and the bulk sending logic.
// src/plivo/plivo.service.ts
import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as plivo from 'plivo';
@Injectable()
export class PlivoService {
private readonly logger = new Logger(PlivoService.name);
private client: plivo.Client;
private defaultSourceNumber: string;
// Plivo's bulk API limit - Verify current limit in Plivo docs!
// Consider making this configurable via .env instead of hardcoding.
private readonly PLIVO_BULK_LIMIT = 1000;
constructor(private configService: ConfigService) {
const authId = this.configService.get<string>('PLIVO_AUTH_ID');
const authToken = this.configService.get<string>('PLIVO_AUTH_TOKEN');
this.defaultSourceNumber = this.configService.get<string>('PLIVO_SOURCE_NUMBER');
if (!authId || !authToken || !this.defaultSourceNumber) {
this.logger.error('Plivo Auth ID, Auth Token, or Source Number missing in environment variables.');
throw new InternalServerErrorException('Plivo configuration is incomplete.');
}
// Note: The plivo-node SDK v4+ uses new plivo.Client().
// This guide assumes v4.x or later. Check plivo-node documentation
// if using a different major version and adjust initialization accordingly.
try {
this.client = new plivo.Client(authId, authToken);
this.logger.log('Plivo client initialized successfully.');
} catch (error) {
this.logger.error('Failed to initialize Plivo client', error.stack);
throw new InternalServerErrorException('Could not initialize Plivo client.');
}
}
/**
* Gets the default source number configured.
*/
getDefaultSourceNumber(): string {
return this.defaultSourceNumber;
}
/**
* Sends a single message to multiple destination numbers using Plivo's bulk API.
* @param destinations - An array of destination phone numbers in E.164 format (e.g., ['+14155552671', '+14155552672']).
* @param message - The text message body to send.
* @param sourceNumber - (Optional) The source number to send from. Defaults to PLIVO_SOURCE_NUMBER from .env.
* @returns The Plivo API response. Type may need adjustment based on SDK version.
* @throws BadRequestException if destination numbers are empty or exceed the limit.
* @throws InternalServerErrorException on Plivo API errors or potential send failures.
*/
async sendBulkSms(
destinations: string[],
message: string,
sourceNumber?: string,
): Promise<plivo.MessageCreateResponse> { // Adjust type based on SDK version if needed
if (!destinations || destinations.length === 0) {
throw new BadRequestException('Destination numbers array cannot be empty.');
}
if (destinations.length > this.PLIVO_BULK_LIMIT) {
this.logger.warn(`Destination count (${destinations.length}) exceeds Plivo limit (${this.PLIVO_BULK_LIMIT}). Consider chunking.`);
// Recommend implementing chunking (see Optimization section) instead of erroring or trimming.
throw new BadRequestException(`Too many destination numbers. Maximum allowed is ${this.PLIVO_BULK_LIMIT}. Implement chunking for larger lists.`);
}
// Format destination numbers with '<' delimiter for Plivo bulk API
const formattedDestinations = destinations.join('<');
const src = sourceNumber || this.defaultSourceNumber;
this.logger.log(`Attempting to send bulk SMS from ${src} to ${destinations.length} numbers.`);
try {
const response = await this.client.messages.create(
src_
formattedDestinations_
message_
);
this.logger.log(`Plivo API raw response: ${JSON.stringify(response)}`);
// VERY IMPORTANT: Basic success check. Plivo's API might return 2xx even if
// messages fail later. The exact structure of a successful response (`messageUuid`
// being present and non-empty) MUST be verified against the specific Plivo SDK
// version and API behavior you are using. This check might need significant adjustment.
// Relying on webhooks (Section 10) for delivery status is more robust.
if (!response || !response.messageUuid || response.messageUuid.length === 0) {
this.logger.error(`Plivo API call succeeded_ but response lacks expected success indicators: ${JSON.stringify(response)}`);
// You might parse response.error or check other fields if Plivo includes them on partial failures.
throw new InternalServerErrorException('Plivo API call succeeded but response suggests message sending failed or requires further checks.');
}
return response;
} catch (error) {
this.logger.error(`Failed to send bulk SMS via Plivo: ${error.message}`_ error.stack);
// Extract more specific error message if available from Plivo SDK error object
const errorMessage = error.response?.data?.error || error.message || 'Unknown Plivo API error';
// Consider mapping Plivo error codes to specific NestJS exceptions if needed
throw new InternalServerErrorException(`Plivo API Error: ${errorMessage}`);
}
}
}
- Why this approach?
- Encapsulates Plivo logic_ making it reusable and testable.
- Injects
ConfigService
for secure credential access. - Initializes the Plivo client once on startup.
- Handles basic validation (empty list_ exceeding limit). Added warning about hardcoded limit.
- Formats the destination numbers correctly (
+1XX<+1YY<+1ZZ
). - Includes logging for monitoring.
- Wraps the API call in
try...catch
for robust error handling. - Added stronger warning about the basic API response check.
PlivoModule
2.3 Update Make the PlivoService
available for injection in other modules.
// src/plivo/plivo.module.ts
import { Module } from '@nestjs/common';
import { PlivoService } from './plivo.service';
// ConfigModule is global_ so no need to import here
@Module({
providers: [PlivoService]_
exports: [PlivoService]_ // Export the service so other modules can use it
})
export class PlivoModule {}
PlivoModule
into AppModule
2.4 Import Ensure PlivoModule
is imported in src/app.module.ts
(as shown in step 1.5).
3. Building the API Layer (Messaging Controller)
Let's create the controller and DTO for our /messaging/bulk-send
endpoint.
3.1 Generate Messaging Module_ Controller_ and DTO
nest generate module messaging
nest generate controller messaging
# No specific CLI command for DTOs_ create manually
3.2 Create the Request DTO
Define the expected shape and validation rules for the incoming request body.
Create src/messaging/dto/send-bulk-sms.dto.ts
:
// src/messaging/dto/send-bulk-sms.dto.ts
import { IsNotEmpty_ IsString_ IsArray_ ArrayNotEmpty_ IsPhoneNumber_ MaxLength_ IsOptional } from 'class-validator';
export class SendBulkSmsDto {
@IsArray()
@ArrayNotEmpty()
@IsPhoneNumber(null_ { each: true_ message: 'Each destination must be a valid phone number in E.164 format (e.g._ +14155552671)' })
destinations: string[]; // Array of phone numbers in E.164 format
@IsString()
@IsNotEmpty()
@MaxLength(1600) // Plivo max length for concatenated SMS
message: string;
@IsOptional() // Make sourceNumber explicitly optional
@IsString()
@IsPhoneNumber(null_ { message: 'If provided_ source number must be a valid phone number in E.164 format' })
sourceNumber?: string; // Optional: Override default source number
}
@IsArray
_@ArrayNotEmpty
: Ensuresdestinations
is a non-empty array.@IsPhoneNumber(null_ { each: true })
: Validates each string in the array is a valid phone number (basic format check_ E.164 recommended).null
uses the default region; specify one like'US'
if needed.@IsString
_@IsNotEmpty
: Ensuresmessage
is a non-empty string.@MaxLength(1600)
: A safeguard based on typical SMS concatenation limits.@IsOptional()
_sourceNumber?
: Clearly markssourceNumber
as optional. Validation applies only if present.
MessagingController
3.3 Implement Define the API endpoint_ inject the PlivoService
_ and use the DTO for validation.
// src/messaging/messaging.controller.ts
import { Controller_ Post_ Body_ HttpCode_ HttpStatus_ Logger } from '@nestjs/common';
import { PlivoService } from '../plivo/plivo.service';
import { SendBulkSmsDto } from './dto/send-bulk-sms.dto';
@Controller('messaging') // Route prefix: /messaging
export class MessagingController {
private readonly logger = new Logger(MessagingController.name);
constructor(private readonly plivoService: PlivoService) {}
@Post('bulk-send') // Route: POST /messaging/bulk-send
@HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as the operation is async
async sendBulkSms(@Body() sendBulkSmsDto: SendBulkSmsDto) {
this.logger.log(`Received bulk SMS request to ${sendBulkSmsDto.destinations.length} destinations.`);
try {
// Use provided sourceNumber or let PlivoService handle the default (passed as undefined)
const result = await this.plivoService.sendBulkSms(
sendBulkSmsDto.destinations_
sendBulkSmsDto.message_
sendBulkSmsDto.sourceNumber_ // Pass optional source
);
// Log the response UUIDs. NOTE: Verify Plivo's bulk response structure.
// `messageUuid` might not always be an array or might represent the batch differently.
const uuids = Array.isArray(result.messageUuid) ? result.messageUuid.join('_ ') : result.messageUuid;
this.logger.log(`Bulk SMS request accepted by Plivo. Message UUID(s): ${uuids}`);
// Return a meaningful response. Adapt based on verified Plivo response structure.
return {
message: 'Bulk SMS request accepted by Plivo.'_
plivoResponse: {
message_uuid: result.messageUuid_ // Forward Plivo's response structure
api_id: result.apiId_
}
};
} catch (error) {
// Error logging is handled in PlivoService_ but we log context here
this.logger.error(`Error processing bulk SMS request: ${error.message}`_ error.stack);
// Re-throw the error so NestJS default exception filter handles it
// Or implement a custom exception filter for finer control
throw error;
}
}
}
@Controller('messaging')
: Sets the base route for this controller.@Post('bulk-send')
: Defines the HTTP method and path for the endpoint.@HttpCode(HttpStatus.ACCEPTED)
: Returns a 202 status code_ suitable for operations handed off to an external system like Plivo.@Body() sendBulkSmsDto: SendBulkSmsDto
: Injects the validated request body using our DTO. TheValidationPipe
configured inmain.ts
handles the validation automatically.- Injects
PlivoService
to perform the actual sending. - Calls
plivoService.sendBulkSms
with data from the DTO. - Returns a confirmation message along with key details from the Plivo response. Added note about verifying
messageUuid
structure.
MessagingModule
3.4 Update Import the PlivoModule
so the controller can inject the PlivoService
.
// src/messaging/messaging.module.ts
import { Module } from '@nestjs/common';
import { MessagingController } from './messaging.controller';
import { PlivoModule } from '../plivo/plivo.module'; // Import PlivoModule
@Module({
imports: [PlivoModule]_ // Make PlivoService available for injection
controllers: [MessagingController]_
providers: []_ // No specific providers needed for this simple module
})
export class MessagingModule {}
MessagingModule
into AppModule
3.5 Import Ensure MessagingModule
is imported in src/app.module.ts
(as shown in step 1.5).
3.6 API Endpoint Testing
You can now run the application (npm run start:dev
) and test the endpoint using curl
or Postman.
Curl Example:
curl -X POST http://localhost:3000/messaging/bulk-send \
-H 'Content-Type: application/json' \
-d '{
""destinations"": [""+14155550100""_ ""+14155550101""]_
""message"": ""Hello from NestJS Plivo Bulk Sender!""_
""sourceNumber"": ""+1XXXXXXXXXX""
}'
# Optional: Omit sourceNumber to use default from .env. REPLACE with your Plivo number if testing override.
Expected Success Response (Status 202 Accepted): (Structure depends on actual Plivo bulk response - verify)
{
""message"": ""Bulk SMS request accepted by Plivo.""_
""plivoResponse"": {
""message_uuid"": [
""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx""_
""yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy""
]_
""api_id"": ""zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz""
}
}
Example Validation Error Response (Status 400 Bad Request):
If you send an invalid phone number:
{
""statusCode"": 400_
""message"": [
""Each destination must be a valid phone number in E.164 format (e.g._ +14155552671)""
]_
""error"": ""Bad Request""
}
4. Integrating with Third-Party Services (Plivo Details)
We've already integrated Plivo via its SDK_ but let's recap the crucial configuration points.
-
API Credentials:
PLIVO_AUTH_ID
: Your unique account identifier. Found on the Plivo Console dashboard under Account.PLIVO_AUTH_TOKEN
: Your secret token for API authentication. Also found on the Plivo Console dashboard. Treat this like a password — do not commit it to code.- How to Obtain: Log in to Plivo Console -> Top-right account menu/icon -> Auth ID and Auth Token are displayed.
- Security: Store these exclusively in environment variables (
.env
for local development, secure environment variables/secrets management in production).
-
Source Phone Number:
PLIVO_SOURCE_NUMBER
: The Plivo phone number (in E.164 format, e.g.,+14155551234
) that will appear as the sender ID for the SMS messages.- How to Obtain: Purchase a number via Plivo Console -> Phone Numbers -> Buy Numbers. Ensure it has SMS capabilities enabled for the target countries. Alternatively, verify an existing number if allowed by Plivo policies.
- Regulations: Sender ID behavior varies by country. Some require pre-registration (Alphanumeric Sender IDs), while others may override the sender ID. Check Plivo's documentation for country-specific regulations.
-
Dashboard Navigation (Recap):
- Log in: https://console.plivo.com/
- Credentials: Top-right corner -> Account section.
- Phone Numbers: Left sidebar -> Phone Numbers -> Manage -> Numbers / Buy Numbers / Verified Numbers.
-
Fallback Mechanisms:
- The current implementation relies solely on Plivo. A robust production system might require fallback to another SMS provider if Plivo experiences an outage.
- Implementation: You would need to:
- Add SDKs and configuration for the alternative provider(s).
- Modify the
PlivoService
(or introduce a higher-levelSmsService
) to catch Plivo errors (specifically network/API availability errors). - In the
catch
block, attempt sending via the fallback provider. - Manage credentials and potentially different API structures for the fallback.
- This adds complexity but increases resilience.
5. Error Handling, Logging, and Retry Mechanisms
Production systems need robust error handling and observability.
-
Error Handling Strategy:
- Validation Errors: Handled by the global
ValidationPipe
andclass-validator
DTOs, returning 400 Bad Request. - Plivo API Errors: Caught within
PlivoService
. Logged with details. Thrown asInternalServerErrorException
(500) or potentiallyBadRequestException
(400) if the error clearly indicates bad input (e.g., invalid number format missed by initial validation, invalid Sender ID). Consider creating custom exceptions (e.g.,PlivoApiException extends HttpException
) for more granular control in exception filters. - Configuration Errors: Checked during
PlivoService
initialization. ThrowInternalServerErrorException
if essential config is missing. - NestJS Exception Filters: Use NestJS's built-in exception filter or create custom filters (
@Catch()
) to format error responses consistently for the API client.
- Validation Errors: Handled by the global
-
Logging:
- NestJS Logger: Use the built-in
Logger
(@nestjs/common
) for simplicity, as shown in the examples. It logs to the console with timestamps, context (class name), and log levels. - Production Logging: For production, integrate a more advanced logging library like
Winston
orPino
with transports to write logs to files or send them to centralized logging platforms (e.g., ELK Stack, Datadog, Logz.io). Configure JSON formatting for easier parsing. - What to Log:
- Incoming requests (method, URL, essential headers/body snippets - sanitize sensitive data).
- Service method calls (e.g.,
sendBulkSms
invocation with parameters - sanitize numbers/message if needed). - Interactions with Plivo (API request details - sanitized, API response summary, latency).
- Errors (full error message, stack trace, relevant context).
- Key successful operations (e.g., ""Bulk SMS request accepted by Plivo"").
- NestJS Logger: Use the built-in
-
Retry Mechanisms:
-
Why Retry? Transient network issues or temporary Plivo API glitches might cause failures. Retrying can improve success rates.
-
CRITICAL WARNING: Retrying a failed Plivo bulk SMS request (
dst=num1<num2<num3...
) without additional mechanisms is highly risky. The basic Plivo bulk API often doesn't provide granular feedback on which specific numbers failed within the batch. Retrying the entire batch after a partial success or ambiguous failure will likely send duplicate messages to recipients who already received the first attempt. -
Recommended Approach:
- Chunking (See Section 9): Send messages in smaller_ manageable batches (e.g._ 50-100 numbers per API call).
- Selective Retry: If a specific chunk fails unambiguously_ you can retry only that chunk. This significantly limits the scope of potential duplicates.
- Idempotency (Difficult with Basic SMS): Plivo's standard message API lacks simple idempotency keys. True idempotency often requires building tracking logic on your side (e.g._ using database logs and unique job IDs).
- Use a Queue: Implement retries within a queue worker (like BullMQ). Queues offer configurable retry strategies (e.g._ exponential backoff) and are better suited for managing background tasks like sending chunks. Combine queuing with chunking for the best results.
-
Illustrative Simple Retry (USE WITH EXTREME CAUTION for bulk sends): The following
async-retry
example demonstrates the concept of retrying an async function. Do not apply this directly to thesendBulkSms
method without implementing chunking and careful consideration of the duplicate message risk.# Example dependency: npm install async-retry @types/async-retry # or # yarn add async-retry @types/async-retry
// Inside a service_ illustrating retry pattern (NOT recommended directly on sendBulkSms) import * as retry from 'async-retry'; import { Logger } from '@nestjs/common'; // Assuming logger is available async function potentiallyUnsafeRetryExample(plivoApiCallFunction: () => Promise<any>) { const logger = new Logger('RetryExample'); // *** WARNING: See text above. Applying simple retry to bulk SMS is dangerous *** // *** due to the risk of duplicate messages. Use with chunking/queues. *** try { const result = await retry( async (bail, attempt) => { logger.log(`Attempting API call, attempt number: ${attempt}`); try { // Replace this with the actual Plivo call *for a single chunk* if using chunking return await plivoApiCallFunction(); } catch (error) { logger.warn(`API attempt ${attempt} failed: ${error.message}`); // Example: Bail (don't retry) on client errors (4xx) if (error.statusCode >= 400 && error.statusCode < 500) { bail(new Error(`Non-retriable Plivo error: ${error.statusCode}`)); return; // Exit after bail } // Rethrow server errors or network issues to trigger retry throw error; } }_ { retries: 3_ // Max attempts factor: 2_ // Exponential backoff minTimeout: 1000_ // Initial delay ms maxTimeout: 5000_ // Max delay ms onRetry: (error_ attempt) => { logger.warn(`Retrying API call after error: ${error.message}. Attempt: ${attempt}`); }, } ); return result; // Return result if retry succeeds } catch (error) { logger.error(`API call failed after all retry attempts: ${error.message}`); throw error; // Rethrow the final error } }
-
6. Creating a Database Schema and Data Layer (Optional)
Storing contact lists or logging message attempts enhances the service. We'll use Prisma and PostgreSQL as an example.
6.1 Install Prisma Dependencies
npm install prisma @prisma/client --save-dev
npm install @nestjs/prisma # Official helper package (optional but convenient)
# or
# yarn add prisma @prisma/client --dev
# yarn add @nestjs/prisma
6.2 Initialize Prisma
npx prisma init --datasource-provider postgresql
This creates a prisma
directory with a schema.prisma
file and updates your .env
with a DATABASE_URL
.
DATABASE_URL
6.3 Configure Update the DATABASE_URL
in your .env
file to point to your PostgreSQL instance.
# .env
# ... other vars
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public
Example: DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/plivo_sms?schema=public
6.4 Define Prisma Schema
Edit prisma/schema.prisma
to define models for contacts and message logs.
// prisma/schema.prisma
generator client {
provider = ""prisma-client-js""
}
datasource db {
provider = ""postgresql""
url = env(""DATABASE_URL"")
}
model Contact {
id Int @id @default(autoincrement())
phoneNumber String @unique @db.VarChar(20) // E.164 format
firstName String?
lastName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messageLogs MessageLog[] // Relation to message logs sent to this contact
}
model BulkMessageJob {
id Int @id @default(autoincrement())
message String @db.Text
sourceNumber String @db.VarChar(20)
status String @default(""PENDING"") // PENDING, PROCESSING, ACCEPTED_BY_PLIVO, COMPLETED_WITH_ERRORS, COMPLETED_SUCCESSFULLY, FAILED
totalRecipients Int
processedCount Int @default(0) // Count accepted by Plivo initially
deliveredCount Int @default(0) // Count confirmed delivered via webhooks
failedCount Int @default(0) // Count confirmed failed/undelivered via webhooks
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messageLogs MessageLog[] // Relation to individual message logs for this job
}
model MessageLog {
id Int @id @default(autoincrement())
job BulkMessageJob @relation(fields: [jobId], references: [id])
jobId Int
contact Contact? @relation(fields: [contactId], references: [id]) // Optional link to a known contact
contactId Int?
destinationNumber String @db.VarChar(20) // Store the number regardless of contact link
plivoMessageUuid String? @unique // Plivo's UUID for this specific message part (if available)
status String @default(""PENDING"") // PENDING, SENT_TO_PLIVO, DELIVERED, FAILED, UNDELIVERED
plivoStatus String? // Store the raw status from Plivo webhook
errorCode String? // Store Plivo error code if applicable
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([jobId])
@@index([contactId])
@@index([plivoMessageUuid])
@@index([status])
}
</p>