code examples
code examples
How to Send Bulk SMS with Twilio Notify, NestJS & Node.js (2025 Guide)
Learn how to send bulk SMS to thousands of recipients using Twilio Notify, NestJS, and Node.js. Step-by-step tutorial with production-ready code for broadcast messaging, error handling, validation, and deployment.
Send Bulk SMS Broadcasts with Twilio Notify, NestJS & Node.js
Learn how to send bulk SMS messages to thousands of recipients using Twilio Notify, NestJS, and Node.js. This production-ready tutorial shows you how to build a broadcast messaging system that handles mass text messaging without rate limits, complete with error handling, validation, database integration, and deployment strategies.
Twilio Notify solves the challenge of efficiently sending identical messages to large recipient lists without managing complex looping logic or hitting API rate limits. This tutorial implements a NestJS REST API that accepts phone numbers and message content, then dispatches broadcasts through Twilio's optimized notification infrastructure.
What You'll Build
- NestJS REST API endpoint for bulk SMS broadcasts
- Twilio Notify service integration with proper error handling
- Input validation using class-validator and DTOs
- Environment-based configuration management
- Database schema for recipient lists and message logs (conceptual)
- Production deployment strategies with Docker
- Rate limiting and retry mechanisms
Prerequisites
- Node.js v18 or v20 LTS
- Twilio account with SMS capabilities
- Basic TypeScript and REST API knowledge
- (Optional) Docker Desktop for containerized deployment
Technologies Used
- Node.js – JavaScript runtime environment
- NestJS – Progressive Node.js framework with modular architecture, dependency injection, and built-in validation support
- Twilio – Cloud communications platform providing SMS, voice, video, and messaging APIs
- TypeScript – Superset of JavaScript adding static typing for improved code quality
- (Optional) Prisma/TypeORM – ORMs for database interaction (demonstrated conceptually)
- (Optional) Docker – Container platform for simplified deployment
System Architecture
+-------------+ +---------------------+ +-----------------+ +----------------+
| Client | -----> | NestJS API Gateway | ---> | MessagingService| ---> | Twilio Notify |
| (e.g., Web, | | (Controller/Auth) | | (Core Logic) | | API |
| Mobile) | +---------------------+ +-----------------+ +----------------+
+-------------+ | |
| (Optional) | (Optional)
v v
+-------------+ +-----------------+
| Database | | Logging Svc |
| (Recipients,| | (e.g., Datadog, |
| Logs) | | Sentry) |
+-------------+ +-----------------+
Final Outcome
By the end of this guide, you'll have a NestJS application with an API endpoint that accepts phone numbers and message content, then uses Twilio Notify to dispatch messages efficiently. The system includes validation, configuration management, error handling, logging, and guidance on testing and deployment.
Note: For production readiness, implement robust authentication/authorization (Section 8) and status callback webhooks (Section 10).
1. Project Setup and Configuration
Initialize your NestJS project and configure the basic structure.
Install NestJS CLI
Install the NestJS command-line interface globally if you haven't already.
npm install -g @nestjs/cliCreate New NestJS Project
Generate a new project using your preferred package manager (npm or yarn).
nest new nestjs-twilio-bulk-sms
cd nestjs-twilio-bulk-smsInstall Dependencies
Install the Twilio Node.js SDK and NestJS configuration module.
npm install twilio @nestjs/config class-validator class-transformer
# or
yarn add twilio @nestjs/config class-validator class-transformerDependencies:
twilio– Official Twilio helper library@nestjs/config– Manages environment variables securelyclass-validator,class-transformer– Validates incoming request data using DTOs
Environment Variable Setup
Create a .env file in the project root for storing sensitive credentials and configuration. Never commit this file to version control. Add .env to your .gitignore file if it's not already there.
# .env
# Twilio Credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_NOTIFY_SERVICE_SID=ISxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Optional: If using a specific Messaging Service for Notify
# TWILIO_MESSAGING_SERVICE_SID=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Application Settings
PORT=3000Configuration values:
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_NOTIFY_SERVICE_SID– Obtained in the next sectionPORT– Port your NestJS application runs on
Configure NestJS ConfigModule
Import and configure the ConfigModule in your main application module (src/app.module.ts) to load variables from the .env file.
// 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 your messaging module later
// import { MessagingModule } from './messaging/messaging.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Make ConfigModule available globally
envFilePath: '.env', // Specify the env file path
}),
// MessagingModule, // Uncomment once created
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Enable ValidationPipe
Enable the global validation pipe in src/main.ts to automatically validate incoming request bodies based on DTOs.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable CORS if needed (adjust origins for production)
app.enableCors();
// Use 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 sent
}),
);
// Get port from ConfigService
const configService = app.get(ConfigService);
const port = configService.get<number>('PORT') || 3000;
await app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();Your basic project structure and configuration are now set up.
2. Twilio Account and Service Setup
Configure the necessary services within the Twilio console to obtain your credentials.
Goal: Obtain TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_NOTIFY_SERVICE_SID.
Get Account Credentials
- Login to Twilio – Go to
https://www.twilio.com/loginand log in to your account - Locate credentials – On your main account dashboard (
https://console.twilio.com/), find yourACCOUNT SIDandAUTH TOKEN - Secure your token – Keep your
AUTH TOKENsecure and treat it like a password - Update .env – Copy these values into your
.envfile
# .env
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Paste your Account SID
TWILIO_AUTH_TOKEN=your_auth_token # Paste your Auth TokenCreate a Messaging Service (Recommended)
While Notify can use your pool of numbers, use a dedicated Messaging Service for sender ID management, content intelligence, and scalability.
- Navigate to Messaging → Services (
https://console.twilio.com/us1/service/sms) - Click Create Messaging Service
- Enter a friendly name (e.g.,
Bulk SMS Service) - Select a use case (e.g.,
Notifications,Marketing) - Click Create Messaging Service
- Go to the Sender Pool section on the service's configuration page
- Add one or more Twilio phone numbers (or Alphanumeric Sender IDs, Short Codes if configured on your account) to this service – you need at least one sender in the pool
Trial account note: Trial accounts can only send messages to verified phone numbers.
- Copy the Messaging Service SID (starts with
MG...) and add it to.envif needed
# .env (Optional)
# TWILIO_MESSAGING_SERVICE_SID=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCreate a Notify Service
This is the core service for bulk messaging.
- Navigate to Engage → Notify → Services (
https://console.twilio.com/us1/develop/notify/services) – search forNotifyor enable it if needed - Click Create Notification Service
- Enter a friendly name (e.g.,
App Broadcasts) - Click Create
- Configure SMS channel – Find the SMS channel configuration section
- Link Messaging Service – Select the Messaging Service SID you created in the previous step from the dropdown – this links Notify to your sender pool
- Click Save
- Copy the Notify Service SID (starts with
IS...) displayed on the service's page - Paste this SID into your
.envfile
# .env
TWILIO_NOTIFY_SERVICE_SID=ISxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Paste your Notify Service SIDYou now have all the necessary Twilio credentials and service SIDs configured in your .env file.
3. NestJS Messaging Module
Create a dedicated module in NestJS to handle all messaging-related logic.
Generate the Module, Service, and Controller
Use the NestJS CLI to generate the necessary files.
nest generate module messaging
nest generate service messaging --no-spec # --no-spec skips test file for brevity
nest generate controller messaging --no-specThis creates:
src/messaging/messaging.module.tssrc/messaging/messaging.service.tssrc/messaging/messaging.controller.ts
Register the Module
Import and add MessagingModule to the imports array in src/app.module.ts.
// 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 { MessagingModule } from './messaging/messaging.module'; // Import
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
MessagingModule, // Add MessagingModule here
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Structure Overview
MessagingController– Handles incoming HTTP requests related to messaging (e.g.,/messaging/bulk-sms), validates input, and calls the serviceMessagingService– Contains the core business logic for interacting with the Twilio API, formats data, and makes API callsMessagingModule– Bundles the controller and service together
4. Implementing the Core Logic (MessagingService)
Implement the service that interacts with Twilio Notify.
// src/messaging/messaging.service.ts
import { Injectable, Logger, InternalServerErrorException, BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Twilio from 'twilio';
@Injectable()
export class MessagingService {
private readonly logger = new Logger(MessagingService.name);
private twilioClient: Twilio.Twilio;
private notifyServiceSid: string;
constructor(private configService: ConfigService) {
const accountSid = this.configService.get<string>('TWILIO_ACCOUNT_SID');
const authToken = this.configService.get<string>('TWILIO_AUTH_TOKEN');
this.notifyServiceSid = this.configService.get<string>('TWILIO_NOTIFY_SERVICE_SID');
if (!accountSid || !authToken || !this.notifyServiceSid) {
this.logger.error('Twilio credentials or Notify Service SID missing in environment variables.');
throw new InternalServerErrorException('Twilio configuration is incomplete.');
}
this.twilioClient = Twilio(accountSid, authToken);
this.logger.log('Twilio client initialized.');
}
/**
* Sends the same SMS message to multiple recipients using Twilio Notify.
* @param recipients - An array of phone numbers in E.164 format (e.g., '+15551234567').
* @param messageBody - The text content of the SMS.
* @returns The SID of the Notification resource created.
* @throws BadRequestException if the recipients array is empty.
* @throws InternalServerErrorException if the message sending fails for other reasons.
* @throws HttpException for specific errors like rate limiting.
*/
async sendBulkSms(recipients: string[], messageBody: string): Promise<string> {
if (!recipients || recipients.length === 0) {
this.logger.warn('Attempted to send bulk SMS with no recipients.');
throw new BadRequestException('No recipients provided.');
}
// Format recipients for Twilio Notify bindings
// See: https://www.twilio.com/docs/notify/api/notification-resource#create-a-notification-resource
const bindings = recipients.map(number => {
return JSON.stringify({ binding_type: 'sms', address: number });
});
this.logger.log(`Sending bulk SMS via Notify to ${recipients.length} recipients.`);
try {
const notification = await this.twilioClient.notify
.services(this.notifyServiceSid)
.notifications.create({
toBinding: bindings,
body: messageBody,
});
this.logger.log(`Successfully created Notification resource: ${notification.sid}`);
return notification.sid;
} catch (error: any) {
this.logger.error(`Failed to send bulk SMS via Twilio Notify: ${error.message}`, error.stack);
// Handle specific Twilio error codes/status
if (error.code === 20003) {
throw new InternalServerErrorException('Twilio authentication failed. Check credentials.');
} else if (error.status === 429) {
this.logger.warn('Twilio rate limit hit. Consider implementing retry logic or queuing.');
throw new HttpException('Rate limit exceeded', HttpStatus.TOO_MANY_REQUESTS);
} else if (error.code === 21211 || error.code === 21604) {
throw new BadRequestException(`Invalid parameter provided to Twilio: ${error.message}`);
}
// Default generic error
throw new InternalServerErrorException(`Failed to send bulk SMS. Error: ${error.message}`);
}
}
// --- Add other messaging methods here (e.g., sendSingleSms) if needed ---
}Key Implementation Details
Dependencies:
- Injects
ConfigServiceto read environment variables - Uses
Loggerfor application logging - Imports exception classes for proper error handling
Constructor:
- Retrieves Twilio credentials (
ACCOUNT_SID,AUTH_TOKEN) andNOTIFY_SERVICE_SIDfrom configuration - Validates these values are present, throwing an error if missing
- Initializes the
Twilioclient using the credentials
sendBulkSms Method:
- Accepts an array of
recipients(phone numbers in E.164 format, e.g.,+15551234567) and themessageBody - Input Validation: Checks if the recipients array is empty and throws a
BadRequestExceptionif it is - Binding Formatting: Maps recipient phone numbers into the JSON structure required by the
toBindingparameter ({ "binding_type": "sms", "address": number }) - API Call: Uses the initialized
twilioClientto callclient.notify.services(SERVICE_SID).notifications.create()toBinding– Formatted array of recipient bindingsbody– Message content
- Logging: Logs success and includes the Notification SID returned by Twilio for tracking
- Error Handling: Catches errors and throws specific exceptions based on Twilio error codes:
20003– Authentication error429– Rate limit exceeded21211/21604– Invalid numbers/parameters- Default – Generic server error
5. Building the API Layer (MessagingController)
Expose the sendBulkSms functionality via a REST API endpoint.
Create Data Transfer Object (DTO)
Define a DTO to specify the expected shape and validation rules for the incoming request body.
// src/messaging/dto/send-bulk-sms.dto.ts
import { IsArray, IsString, IsNotEmpty, ArrayNotEmpty, IsPhoneNumber } from 'class-validator';
// Remove swagger imports if not used, or keep if using Swagger
// import { ApiProperty } from '@nestjs/swagger';
export class SendBulkSmsDto {
@IsArray()
@ArrayNotEmpty()
@IsPhoneNumber(null, { each: true, message: 'Each recipient must be a valid phone number in E.164 format (e.g., +15551234567)' })
// @ApiProperty({
// description: 'An array of recipient phone numbers in E.164 format.',
// example: ['+15551234567', '+447700900123'],
// type: [String],
// })
recipients: string[];
@IsString()
@IsNotEmpty()
// @ApiProperty({
// description: 'The text content of the SMS message.',
// example: 'Hello from our NestJS app!',
// })
message: string;
}Validation Rules
The DTO uses class-validator decorators to enforce rules:
| Decorator | Purpose |
|---|---|
@IsArray, @ArrayNotEmpty | Ensures recipients is a non-empty array |
@IsPhoneNumber(null, { each: true }) | Validates each string in the recipients array is a valid phone number using libphonenumber-js. The null parameter accepts any country code when numbers use E.164 format |
@IsString, @IsNotEmpty | Ensures message is a non-empty string |
Swagger Integration (Optional): Uncomment the @ApiProperty decorators if you integrate Swagger for API documentation.
Implement the Controller
Create the endpoint in MessagingController.
// src/messaging/messaging.controller.ts
import { Controller, Post, Body, UsePipes, ValidationPipe, Logger, HttpCode, HttpStatus } from '@nestjs/common';
import { MessagingService } from './messaging.service';
import { SendBulkSmsDto } from './dto/send-bulk-sms.dto';
// Remove swagger imports if not used, or keep if using Swagger
// import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
// @ApiTags('Messaging') // Group endpoints in Swagger UI
@Controller('messaging')
export class MessagingController {
private readonly logger = new Logger(MessagingController.name);
constructor(private readonly messagingService: MessagingService) {}
@Post('bulk-sms')
@HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as the process is async
// @ApiOperation({ summary: 'Send a bulk SMS message via Twilio Notify' })
// @ApiResponse({ status: HttpStatus.ACCEPTED, description: 'Request accepted, notification creation initiated.' })
// @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid input data (e.g., missing fields, invalid phone numbers, empty recipients array).' })
// @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Failed to initiate bulk SMS due to server or Twilio error.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async sendBulkSms(@Body() sendBulkSmsDto: SendBulkSmsDto): Promise<{ message: string; notificationSid?: string }> {
this.logger.log(`Received request to send bulk SMS to ${sendBulkSmsDto.recipients.length} recipients.`);
const { recipients, message } = sendBulkSmsDto;
try {
const notificationSid = await this.messagingService.sendBulkSms(recipients, message);
return {
message: 'Bulk SMS request accepted by Twilio Notify.',
notificationSid: notificationSid,
};
} catch (error) {
this.logger.error(`Error processing bulk SMS request: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined);
throw error;
}
}
}Controller Details
Dependencies:
- Injects
MessagingService - Uses
Loggerfor request logging
Routing:
@Controller('messaging')sets the base path to/messaging@Post('bulk-sms')defines a POST endpoint at/messaging/bulk-sms@HttpCode(HttpStatus.ACCEPTED)sets the default success status code to 202 Accepted (appropriate for asynchronous processes)
Request Processing:
@Body() sendBulkSmsDto: SendBulkSmsDtobinds the incoming JSON request body to the DTOValidationPipeautomatically validates the object – validation failures return 400 Bad Request- Logs the incoming request with recipient count
- Destructures validated
recipientsandmessagefrom the DTO - Calls
messagingService.sendBulkSms - Returns a success response containing a message and the
notificationSid - Catches and re-throws errors for NestJS exception filters to handle
Setup Swagger (Optional)
For API documentation, install Swagger:
npm install @nestjs/swagger swagger-ui-express
# or
yarn add @nestjs/swagger swagger-ui-expressConfigure it in src/main.ts:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
// Swagger Setup
const config = new DocumentBuilder()
.setTitle('Bulk SMS API')
.setDescription('API for sending bulk SMS messages via Twilio Notify')
.setVersion('1.0')
.addTag('Messaging')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
const configService = app.get(ConfigService);
const port = configService.get<number>('PORT') || 3000;
await app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`);
console.log(`Swagger Docs available at: ${await app.getUrl()}/api-docs`);
}
bootstrap();Access interactive API documentation at /api-docs when you run the app. Remember to uncomment the @ApiTags, @ApiOperation, @ApiResponse, and @ApiProperty decorators in the controller and DTO.
6. Database Integration (Conceptual)
This section outlines the conceptual steps for database integration using Prisma. A full implementation is beyond the scope of this core guide but represents a typical next step for production applications.
Real-world applications often need to:
- Store Recipient Lists – Manage groups of users
- Log Message Status – Track broadcast status (e.g., initiated, completed, failed)
Install Prisma
npm install prisma @prisma/client --save-dev
# or
yarn add prisma @prisma/client -DInitialize Prisma
npx prisma init --datasource-provider postgresql # or your chosen DBThis creates a prisma directory with schema.prisma and updates .env with DATABASE_URL. Configure your DATABASE_URL in .env.
Define Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Recipient {
id String @id @default(cuid())
phoneNumber String @unique // E.164 format
firstName String?
lastName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Add relationships to groups/lists if needed
// Example: recipientListId String?
// Example: recipientList RecipientList? @relation(fields: [recipientListId], references: [id])
}
// Example List Model
// model RecipientList {
// id String @id @default(cuid())
// name String @unique
// description String?
// recipients Recipient[]
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// }
model BulkMessageLog {
id String @id @default(cuid())
twilioNotifySid String @unique // The SID from Twilio Notify
messageBody String
status String @default("initiated") // e.g., initiated, completed, failed
recipientCount Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Apply Migrations
npx prisma migrate dev --name init-messaging-schemaGenerate Prisma Client
npx prisma generateCreate Prisma Service
Abstract database interactions into a service.
nest generate service prisma --no-spec// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PrismaService.name);
async onModuleInit() {
await this.$connect();
this.logger.log('Prisma client connected.');
}
async onModuleDestroy() {
await this.$disconnect();
this.logger.log('Prisma client disconnected.');
}
}Register PrismaService in the modules where it's needed (e.g., MessagingModule) and ensure PrismaModule is created and imported globally or locally.
Use in Messaging Service
Modify MessagingService to:
- Accept a list ID and fetch recipients from the database using
PrismaService - Log the broadcast details to
BulkMessageLogafter initiating the send
// Example modification in MessagingService
import { PrismaService } from '../prisma/prisma.service'; // Adjust path as needed
// ... inside MessagingService class
constructor(
private configService: ConfigService,
private prisma: PrismaService, // Inject PrismaService
) {
// ... Twilio setup
}
// Example method using DB lookup (conceptual)
async sendBulkSmsToList(listId: string, messageBody: string): Promise<string> {
// 1. Fetch recipients from DB based on listId
const recipientsData = await this.prisma.recipient.findMany({
// where: { recipientListId: listId }, // Example filter if using lists
select: { phoneNumber: true }
});
const recipients = recipientsData.map(r => r.phoneNumber);
if (recipients.length === 0) {
throw new BadRequestException(`No recipients found for list ID: ${listId}`);
}
// 2. Call existing logic to send SMS
const notificationSid = await this.sendBulkSms(recipients, messageBody);
// 3. Log the attempt
try {
await this.prisma.bulkMessageLog.create({
data: {
twilioNotifySid: notificationSid,
messageBody: messageBody,
recipientCount: recipients.length,
status: 'initiated',
},
});
this.logger.log(`Logged bulk message initiation for SID: ${notificationSid}`);
} catch (dbError) {
this.logger.error(`Failed to log bulk message initiation for SID ${notificationSid}: ${dbError instanceof Error ? dbError.message : String(dbError)}`, dbError instanceof Error ? dbError.stack : undefined);
// Decide how to handle logging failure: log the error and continue,
// or throw an error if logging is critical.
}
return notificationSid;
}Reminder: This is conceptual. A production system requires building out list management APIs and integrating status updates (likely via webhooks, see Section 10).
7. Error Handling, Logging, and Retry Mechanisms
Production systems need robust error handling and logging.
Error Handling
| Error Type | Handling Method | HTTP Status |
|---|---|---|
| Validation Errors | ValidationPipe (automatic) | 400 Bad Request |
| Empty Recipient Errors | MessagingService throws BadRequestException | 400 Bad Request |
| Twilio API Errors | try...catch block in MessagingService checks specific error.code and error.status values | 400, 429, 500 |
| Configuration Errors | Constructor check in MessagingService on startup | 500 Internal Server Error |
See Twilio Error Codes for a complete list.
Logging
NestJS's built-in Logger logs to the console by default.
Production Logging: Integrate a robust logging solution (e.g., Winston, Pino) and configure it to:
- Log to files or external services (Datadog, Sentry, ELK stack)
- Use JSON format for easier parsing
- Include context (request ID, user ID if applicable)
- Adjust log levels (
debug,info,warn,error)
Key Logging Points:
| Location | Event | Log Level |
|---|---|---|
MessagingController | Incoming requests | info |
MessagingService constructor | Configuration loading/validation | info/error |
MessagingService | Initiation of bulk send with recipient count | info |
MessagingService | Successful notification creation with SID | info |
MessagingService | Errors during Twilio API calls with details | error |
| Database operations | Logging successes/failures | info/error |
MessagingService | Empty recipient list warnings | warn |
Retry Mechanisms
Twilio API calls can fail transiently (network issues, temporary Twilio hiccups, rate limits – 429).
Considerations:
Idempotency: Twilio Notify's create operation is generally not idempotent. Retrying the same create call might result in duplicate broadcasts.
Strategy: Instead of retrying the create call directly for transient errors, consider:
- Queuing – Implement a background job queue (e.g., BullMQ, RabbitMQ). If the initial API call fails transiently, place the job in the queue to retry later with exponential backoff.
- Client-Side Retry – The client calling your API can implement retries if they receive a
5xxor429response. - Status Check & Resend – If a call fails, log the failure. A separate process can later check the status of intended recipients (if tracked) and attempt to resend only to those who didn't receive the message (more complex).
Simple Retry (Use with Caution): If you must implement a simple retry within the service (e.g., for immediate network errors), use a library like async-retry or implement carefully with backoff. Avoid retrying for non-transient errors (auth, bad parameters) or potentially duplicate-creating operations like Notify create unless you have specific logic to handle it.
Queue-Based Retry (Recommended)
// Conceptual retry logic using a queue (requires setting up BullMQ or similar)
// In MessagingController:
// constructor(
// private readonly messagingService: MessagingService,
// @InjectQueue('message-queue') private messageQueue: Queue, // Inject queue
// ) {}
// async sendBulkSms(@Body() sendBulkSmsDto: SendBulkSmsDto) {
// this.logger.log(`Queueing request to send bulk SMS to ${sendBulkSmsDto.recipients.length} recipients.`);
// await this.messageQueue.add('send-bulk-sms-job', {
// recipients: sendBulkSmsDto.recipients,
// message: sendBulkSmsDto.message,
// }, {
// attempts: 3, // Max attempts
// backoff: { type: 'exponential', delay: 1000 }, // Exponential backoff
// });
// return { message: 'Bulk SMS request queued for processing.' };
// // HttpStatus remains 202 Accepted
// }
// Then create a Queue Processor that calls messagingService.sendBulkSmsFor production, use a dedicated job queue – it's the most robust approach for handling retries of potentially non-idempotent operations like sending notifications.
Frequently Asked Questions About Bulk SMS with Twilio Notify
What is Twilio Notify and how does it differ from the Twilio SMS API?
Twilio Notify is a specialized service designed for sending identical notifications to large recipient lists across multiple channels (SMS, push notifications, email). Unlike the standard Twilio SMS API that requires individual API calls per recipient, Notify accepts a single request with multiple bindings and handles message distribution efficiently, avoiding rate limits and complex looping logic. Use Notify for bulk broadcasts and the SMS API for individual, personalized messages.
How many recipients can I send to in a single Twilio Notify request?
Twilio Notify supports sending to up to 10,000 recipients in a single API call via the toBinding parameter. For larger audiences, split your recipient list into batches of 10,000 and make multiple Notify requests. Each request returns a unique Notification SID for tracking delivery status.
Do I need a Twilio Messaging Service to use Notify?
Yes, Twilio Notify requires a Messaging Service with at least one phone number in the sender pool. The Messaging Service (SID starting with MG) provides sender ID management, content intelligence, and scalability features. Configure your Notify Service to use the Messaging Service through the Twilio Console under the SMS channel settings.
How do I validate phone numbers in E.164 format with NestJS?
Use the @IsPhoneNumber(null, { each: true }) decorator from class-validator in your DTO. This validates each recipient phone number using libphonenumber-js. The null parameter accepts any country code when numbers use E.164 format (e.g., +15551234567 for US, +447700900123 for UK). The { each: true } option validates every element in the recipients array.
What HTTP status code should my bulk SMS endpoint return?
Return 202 Accepted rather than 200 OK because Twilio Notify processes messages asynchronously. The API call initiates the broadcast but doesn't guarantee immediate delivery to all recipients. A 202 status correctly indicates the server accepted the request for processing without confirming completion.
How do I handle Twilio API rate limits in production?
Implement a job queue (BullMQ, RabbitMQ, or AWS SQS) to handle requests exceeding Twilio's rate limits. When you receive a 429 Too Many Requests error, queue the failed request for retry with exponential backoff. The error handling code in this guide detects error.status === 429 and throws an HttpException that your queue processor can catch and reschedule.
Can I track individual message delivery status with Notify?
Yes, but it requires additional configuration. Implement status callback webhooks by configuring your Messaging Service's callback URL to receive delivery receipts for each message. Create a webhook endpoint in your NestJS application that processes MessageStatus events from Twilio, then update your database with delivery statuses (sent, delivered, failed, undelivered).
Should I store Twilio credentials in environment variables or a secrets manager?
For development, use .env files with the @nestjs/config module. For production, use a secrets manager like AWS Secrets Manager, HashiCorp Vault, or Google Secret Manager. Never commit credentials to version control. The NestJS ConfigService can integrate with secrets managers through custom configuration loaders.
How do I implement authentication for my bulk SMS API endpoint?
Add NestJS Guards for authentication. Implement JWT-based authentication using @nestjs/passport and passport-jwt, or API key authentication with a custom guard. Apply guards to your controller with @UseGuards(JwtAuthGuard) or globally in main.ts. Always validate the authenticated user has permission to send bulk messages before processing requests.
What's the best way to test Twilio Notify integration without sending real SMS?
Use Twilio's test credentials for unit tests or mock the Twilio client in your service tests. For integration testing, purchase a Twilio phone number and send messages to verified numbers on your account (trial accounts can only send to verified numbers). Alternatively, use Twilio's webhook testing tools like twilio-run or ngrok to test callbacks locally without actual SMS delivery.
Next Steps for Your Bulk SMS System
Enhance your bulk SMS broadcast system with these production-ready features:
- Implement Authentication & Authorization – Add JWT-based authentication with
@nestjs/passportand role-based access control to restrict bulk messaging to authorized users - Set Up Status Webhooks – Configure Twilio callback URLs to receive delivery receipts and update your database with real-time message status
- Add Recipient List Management – Build CRUD endpoints for managing recipient groups, importing CSV files, and handling opt-outs with proper consent tracking
- Integrate Job Queues – Implement BullMQ or RabbitMQ to handle large broadcast jobs asynchronously with retry logic and progress tracking
- Implement Rate Limiting – Add
@nestjs/throttlerto protect your API from abuse and prevent excessive Twilio charges - Set Up Monitoring & Alerts – Integrate Datadog, Sentry, or New Relic to track API performance, error rates, and Twilio spending
- Add Message Scheduling – Allow users to schedule broadcasts for future delivery with timezone support using job queues with delayed execution
- Build Analytics Dashboard – Track metrics like delivery rates, failed messages, recipient engagement, and cost per message
Additional Resources
- Twilio Notify API Documentation
- NestJS Official Documentation
- Twilio Node.js SDK Reference
- BullMQ Job Queue Documentation
- How to Send SMS with Node.js – Learn basic SMS sending before bulk operations
- NestJS Validation Best Practices – Deep dive into DTO validation strategies
Your NestJS bulk SMS system is now ready for production deployment with proper error handling, validation, and scalability considerations.
Frequently Asked Questions
how to send bulk sms with twilio and node.js
Use the Twilio Notify service with the Twilio Node.js SDK and a backend framework like NestJS. This setup allows you to send the same SMS message to many recipients without complex client-side looping or API rate limit issues, as the article details. You'll need a Twilio account, Node.js, and basic knowledge of REST APIs and TypeScript.
what is twilio notify used for
Twilio Notify is a service specifically designed for sending bulk notifications, including SMS messages, to large groups of recipients. It simplifies the process and handles complexities like API rate limits, making it ideal for broadcast scenarios as described in the article. You'll need a Notify Service SID to use it within your application.
why use nestjs for bulk sms sending
NestJS provides a robust, structured framework for building scalable server-side applications in Node.js. Its modular architecture, dependency injection, and built-in features like validation pipes streamline the development process for the bulk SMS application as shown in the article. It is well-suited for handling API requests and managing complex logic like Twilio integration.
how to set up twilio account for bulk sms
First, log into your Twilio account. Then, retrieve your Account SID and Auth Token from the dashboard. Optionally, create a Messaging Service for better sender ID management, and finally create a Notify Service, ensuring the SMS channel is enabled and linked to your messaging service or a Twilio phone number. These values will be required within your `.env` file, as covered in the article.
what are twilio messaging services
Twilio Messaging Services provide a way to group phone numbers or Alphanumeric Sender IDs and configure additional messaging features. Using a Messaging Service is best practice, especially for bulk messaging with Twilio Notify. The article recommends this for features like sender ID management and content intelligence, but it may not be strictly required depending on your Notify configuration.
how to integrate prisma with nestjs twilio app
Install the required Prisma packages, initialize Prisma using `npx prisma init`, define your data models in `schema.prisma`, apply migrations using `npx prisma migrate dev`, generate the Prisma Client, create a Prisma service, and inject it into your NestJS modules. This allows database operations like storing recipient lists and logging message statuses.
when to use a database for sms application
Consider using a database when you need to manage lists of recipients, store message logs, track delivery statuses, or implement other features beyond a simple one-off bulk send. The article suggests using a database like PostgreSQL with Prisma or TypeORM as your application scales and requires persistent data storage.
what is the role of class-validator in nestjs
Class-validator provides decorators for implementing validation rules in DTOs (Data Transfer Objects). In the bulk SMS application, this ensures the incoming request data is in the correct format and non-empty, improving application security and preventing unexpected behavior.
how to handle twilio api errors in nestjs
Wrap Twilio API calls in a `try...catch` block and handle specific error codes and HTTP statuses returned by Twilio. The article recommends checking for common Twilio error codes (e.g. `20003` for auth, `429` for rate limits) or statuses like `429` and `21211` and throwing appropriate exceptions in NestJS (e.g., `BadRequestException`, `HttpException`, `InternalServerErrorException`) based on those codes and statuses.
why use e.164 phone number format with twilio
E.164 is an international standard format for phone numbers (e.g., +15551234567). It ensures consistent formatting across regions and is required by Twilio for accurate message delivery. Using this standard format from the start improves compatibility and reduces issues, as described in the article.
can i use docker for deploying nestjs twilio application
Yes, Docker simplifies deployment by containerizing your application and its dependencies. While optional, the article mentions Docker as a good practice for consistent environments across development, testing, and production. A basic understanding of Docker is beneficial for using this option.
how to implement retry logic for twilio api calls
Due to idempotency concerns with Twilio Notify's `create` operation, it's better to use a message queue (like BullMQ or RabbitMQ). If the initial API call fails transiently, add the task to the queue for retry with exponential backoff, rather than immediately retrying `notifications.create`. The article covers some considerations regarding idempotency and simple retry strategies for immediate network errors if you must.