code examples
code examples
Build Bulk SMS Broadcasting with Plivo and NestJS: Complete Tutorial
Learn how to send bulk SMS messages using Plivo API and NestJS. Step-by-step guide covering setup, error handling, validation, and database integration for broadcast messaging systems.
Build a Bulk SMS Broadcasting System with Plivo and NestJS
Learn how to build a production-ready bulk SMS service using Plivo's API and NestJS. This comprehensive tutorial guides you through creating a scalable broadcast messaging system, from initial setup through database integration. You'll implement secure credential management, request validation, error handling, and efficient bulk sending patterns that handle hundreds of recipients per API call.
What you'll build: A NestJS REST API that sends SMS messages to multiple recipients simultaneously using Plivo's bulk messaging capabilities, complete with TypeScript type safety, DTO validation, and database logging.
Note: This guide uses plivo-node v4.67.0+ syntax. Always verify your installed SDK version matches the code examples. The Plivo SDK is actively maintained, so check the npm package page for the latest stable version before starting.
IMPORTANT: This article is a comprehensive technical guide but ends at the database schema section (Prisma setup). Additional topics like deployment strategies, production monitoring, webhook implementation for delivery status, and advanced queuing patterns require separate documentation. Use this guide as a foundation and refer to Plivo's official documentation and NestJS best practices for production deployment.
What You're Building
Build a NestJS application with a dedicated API endpoint that accepts a list of phone numbers and a message body. Your service will 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).
Problems This Service Solves
Send the same SMS message to multiple recipients simultaneously without making individual API calls for each number. Use this pattern for:
- Marketing campaigns and promotional broadcasts
- Emergency alerts and critical notifications
- Appointment reminders at scale
- System status updates and service announcements
Technologies You'll Use
- Node.js: JavaScript runtime for server-side applications
- NestJS: Progressive Node.js framework for building scalable server-side applications with TypeScript
- TypeScript: Static typing for improved code quality and developer experience
- Plivo &
plivo-nodeSDK: Cloud communications platform for sending SMS (Guide uses v4.x) - dotenv &
@nestjs/config: Secure environment variable and configuration management class-validator&class-transformer: Robust request payload validation- (Optional but Recommended) Redis & BullMQ: Message queuing for high-throughput scenarios
- (Optional) Prisma & PostgreSQL: Contact list management and message history logging
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;
endPrerequisites
- Node.js (LTS version recommended: v20.x or v22.x as of 2025) 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/cli1.2 Create New NestJS Project
nest new plivo-bulk-sms
cd plivo-bulk-smsChoose 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.envfile 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.
1.4 Environment Setup (.env)
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.
SECURITY: Add .env to your .gitignore file immediately after creating it:
# Add to .gitignore
.env
.env.local
.env.*.local# .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
Accountsection in the top right menu. - Your
Auth IDandAuth Tokenare displayed prominently. - Purchase or verify a phone number under
Phone Numbers->Buy NumbersorVerified Numbers. Use this as yourPLIVO_SOURCE_NUMBER.
1.5 Configure ConfigModule
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 importConfigModuleinto 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 plivoThis creates src/plivo/plivo.module.ts and src/plivo/plivo.service.ts.
2.2 Implement PlivoService
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 - IMPORTANT: Verify the current limit in Plivo's official documentation
// at https://www.plivo.com/docs/sms/api/message/ as limits may change.
// Common limit is 1000 recipients per API call, but always verify for your account tier.
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.67.0 or later. Verify your installed version:
// npm list plivo or check package.json
// If using a different major version, consult plivo-node GitHub repository
// for correct initialization syntax.
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 should be verified against your plivo-node 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<any> { // Changed from plivo.MessageCreateResponse - verify exact type with your SDK version
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 configured 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 (v4.67.0+) and API behavior you are using. This check might need adjustment.
// Response structure can vary: messageUuid might be a string or array depending on
// whether you sent to single or multiple recipients.
// Relying on webhooks for delivery status is the most robust approach.
if (!response || !response.messageUuid) {
this.logger.error(`Plivo API call succeeded, but response lacks expected messageUuid: ${JSON.stringify(response)}`);
throw new InternalServerErrorException('Plivo API response missing messageUuid. Verify SDK response structure.');
}
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';
throw new InternalServerErrorException(`Plivo API Error: ${errorMessage}`);
}
}
}- Why this approach?
- Encapsulates Plivo logic, making it reusable and testable.
- Injects
ConfigServicefor 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...catchfor robust error handling. - Added stronger warning about the basic API response check.
2.3 Update PlivoModule
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 {}2.4 Import PlivoModule into AppModule
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 manually3.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: Ensuresdestinationsis 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).nulluses the default region; specify one like'US'if needed.@IsString,@IsNotEmpty: Ensuresmessageis a non-empty string.@MaxLength(1600): A safeguard based on typical SMS concatenation limits.@IsOptional(),sourceNumber?: Clearly markssourceNumberas optional. Validation applies only if present.
3.3 Implement MessagingController
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. TheValidationPipeconfigured inmain.tshandles the validation automatically.- Injects
PlivoServiceto perform the actual sending. - Calls
plivoService.sendBulkSmswith data from the DTO. - Returns a confirmation message along with key details from the Plivo response. Added note about verifying
messageUuidstructure.
3.4 Update MessagingModule
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 {}3.5 Import MessagingModule into AppModule
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 for your SDK version - 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"
}
}Note: The message_uuid field might be a single string or an array depending on your Plivo SDK version and number of recipients. Always log and verify the actual response structure during development.
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 (
.envfor 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
catchblock, 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
ValidationPipeandclass-validatorDTOs, 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
PlivoServiceinitialization. ThrowInternalServerErrorExceptionif 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
WinstonorPinowith 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.,
sendBulkSmsinvocation 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-retryexample demonstrates the concept of retrying an async function. Do not apply this directly to thesendBulkSmsmethod without implementing chunking and careful consideration of the duplicate message risk.bash# Example dependency: npm install async-retry @types/async-retry # or # yarn add async-retry @types/async-retrytypescript// 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 history 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/prisma6.2 Initialize Prisma
npx prisma init --datasource-provider postgresqlThis creates a prisma directory with a schema.prisma file and updates your .env with a DATABASE_URL.
6.3 Configure DATABASE_URL
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=publicExample: 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])
}ARTICLE CONTINUATION NOTE: This guide ends here with the database schema definition. For a complete production implementation, you'll need to:
- Run Prisma migrations:
npx prisma migrate dev --name initto create database tables - Generate Prisma Client:
npx prisma generateto create type-safe database access - Implement PrismaService: Create a NestJS service to handle database operations
- Add webhook endpoints: Implement routes to receive Plivo delivery status updates
- Implement message queuing: Use BullMQ or similar for high-throughput scenarios
- Add comprehensive logging: Integrate Winston or Pino for production logging
- Deploy with Docker: Containerize the application for consistent deployment
- Set up monitoring: Implement health checks and observability
Additional Resources:
- Plivo SMS API Documentation
- Plivo Node.js SDK GitHub
- NestJS Official Documentation
- Prisma Documentation
- BullMQ Documentation
Security Reminders:
- Never commit
.envfiles or credentials to version control - Use environment-specific configuration for development, staging, and production
- Implement rate limiting on your API endpoints
- Validate and sanitize all user inputs
- Use HTTPS in production
- Rotate API credentials regularly
- Monitor for unusual API usage patterns
This guide provides a solid foundation for building a bulk SMS service with Plivo and NestJS. Expand upon these patterns based on your specific requirements and scale needs.
Frequently Asked Questions
How to send bulk SMS with NestJS?
Use NestJS with the Plivo Node.js SDK to create a service that interacts with the Plivo API. This allows you to send a single message to multiple recipients efficiently through an API endpoint designed for bulk messaging, improving the process for mass communication.
What is Plivo Node.js SDK v4 used for?
The Plivo Node.js SDK v4 is the official helper library used to interact with the Plivo API from a Node.js application. It simplifies sending SMS messages and provides tools for other communication services, streamlining integration with Plivo.
Why use NestJS for bulk SMS service?
NestJS provides a structured and scalable architecture that is beneficial for services requiring reliability and maintainability, like bulk SMS sending. Its modular design and TypeScript support enhance code organization and developer experience.
When to use a message queue with Plivo?
Message queues (Redis, BullMQ) are beneficial when sending large volumes of SMS messages. They enable asynchronous processing, enhancing application responsiveness, especially during peak demand, and offer better retry mechanisms.
Can I use a different SMS provider as a fallback?
Yes, it's possible and recommended for production to include a fallback mechanism. Implement logic in the service layer to catch errors with the primary provider (Plivo) and then attempt sending via an alternative SMS gateway.
How to set up Plivo credentials in NestJS?
Store your Plivo Auth ID and Auth Token in environment variables, using a .env file locally. In production, leverage a secure secrets management solution. Access these via NestJS's ConfigModule for secure integration.
What is the maximum number of recipients allowed per Plivo API request?
While the article suggests a limit of 1000 recipients, always check current Plivo documentation. If sending to more, implement chunking (dividing recipients into smaller groups) and send multiple bulk requests. The provided implementation warns about potential issues with exceeding the limit.
How to log Plivo API interactions in NestJS?
Leverage NestJS's built-in Logger or an external logging library like Winston or Pino. Log incoming API requests, Plivo API responses, and any errors or successful operations, including timestamps and relevant context.
How does request validation work for the sendBulkSms endpoint?
A DTO (Data Transfer Object) along with the class-validator and class-transformer packages provide request validation. The ValidationPipe, enabled globally, ensures that incoming requests adhere to predefined rules, enhancing application security.
What does a 202 Accepted response mean in this context?
A 202 Accepted HTTP status code indicates that the bulk SMS request has been received for processing but is not yet completed. Because SMS delivery is asynchronous, the 202 status confirms the server has acknowledged the request.
What is chunking and when do I need it for sending bulk SMS?
Chunking involves splitting a large list of recipients into smaller groups. If exceeding the Plivo bulk API recipient limit, implement chunking to send messages in separate, smaller batches, maintaining functionality.
How to handle Plivo API errors in my service?
Use a try...catch block to handle errors from the Plivo SDK. Log the error details and return an appropriate HTTP error response, such as a 500 Internal Server Error, to provide client feedback.
How to implement retry mechanisms for Plivo API calls?
Use queuing systems like BullMQ, which offer retry strategies. Combine queuing with chunking to allow safe retries without sending duplicate messages.
What database is suggested for storing contact data?
The article recommends Prisma with PostgreSQL for data persistence, and provides a sample schema. This enables managing contact lists efficiently and logging message history for enhanced monitoring.
How to obtain a Plivo phone number for sending SMS?
Purchase a phone number via Plivo Console, ensure it allows SMS sending to your target region, and configure it as the source number for sending messages. Review Plivo documentation for sender ID and country-specific rules.