code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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

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.

bash
npm install -g @nestjs/cli

Create New NestJS Project

Generate a new project using your preferred package manager (npm or yarn).

bash
nest new nestjs-twilio-bulk-sms
cd nestjs-twilio-bulk-sms

Install Dependencies

Install the Twilio Node.js SDK and NestJS configuration module.

bash
npm install twilio @nestjs/config class-validator class-transformer
# or
yarn add twilio @nestjs/config class-validator class-transformer

Dependencies:

  • twilio – Official Twilio helper library
  • @nestjs/config – Manages environment variables securely
  • class-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.

dotenv
# .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=3000

Configuration values:

  • TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_NOTIFY_SERVICE_SID – Obtained in the next section
  • PORT – 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.

typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
// Import 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.

typescript
// 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

  1. Login to Twilio – Go to https://www.twilio.com/login and log in to your account
  2. Locate credentials – On your main account dashboard (https://console.twilio.com/), find your ACCOUNT SID and AUTH TOKEN
  3. Secure your token – Keep your AUTH TOKEN secure and treat it like a password
  4. Update .env – Copy these values into your .env file
dotenv
# .env
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Paste your Account SID
TWILIO_AUTH_TOKEN=your_auth_token                 # Paste your Auth Token

While Notify can use your pool of numbers, use a dedicated Messaging Service for sender ID management, content intelligence, and scalability.

  1. Navigate to Messaging → Services (https://console.twilio.com/us1/service/sms)
  2. Click Create Messaging Service
  3. Enter a friendly name (e.g., Bulk SMS Service)
  4. Select a use case (e.g., Notifications, Marketing)
  5. Click Create Messaging Service
  6. Go to the Sender Pool section on the service's configuration page
  7. 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.

  1. Copy the Messaging Service SID (starts with MG...) and add it to .env if needed
dotenv
# .env (Optional)
# TWILIO_MESSAGING_SERVICE_SID=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Create a Notify Service

This is the core service for bulk messaging.

  1. Navigate to Engage → Notify → Services (https://console.twilio.com/us1/develop/notify/services) – search for Notify or enable it if needed
  2. Click Create Notification Service
  3. Enter a friendly name (e.g., App Broadcasts)
  4. Click Create
  5. Configure SMS channel – Find the SMS channel configuration section
  6. Link Messaging Service – Select the Messaging Service SID you created in the previous step from the dropdown – this links Notify to your sender pool
  7. Click Save
  8. Copy the Notify Service SID (starts with IS...) displayed on the service's page
  9. Paste this SID into your .env file
dotenv
# .env
TWILIO_NOTIFY_SERVICE_SID=ISxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Paste your Notify Service SID

You 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.

bash
nest generate module messaging
nest generate service messaging --no-spec # --no-spec skips test file for brevity
nest generate controller messaging --no-spec

This creates:

  • src/messaging/messaging.module.ts
  • src/messaging/messaging.service.ts
  • src/messaging/messaging.controller.ts

Register the Module

Import and add MessagingModule to the imports array in src/app.module.ts.

typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { 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 service
  • MessagingService – Contains the core business logic for interacting with the Twilio API, formats data, and makes API calls
  • MessagingModule – Bundles the controller and service together

4. Implementing the Core Logic (MessagingService)

Implement the service that interacts with Twilio Notify.

typescript
// 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 ConfigService to read environment variables
  • Uses Logger for application logging
  • Imports exception classes for proper error handling

Constructor:

  • Retrieves Twilio credentials (ACCOUNT_SID, AUTH_TOKEN) and NOTIFY_SERVICE_SID from configuration
  • Validates these values are present, throwing an error if missing
  • Initializes the Twilio client using the credentials

sendBulkSms Method:

  • Accepts an array of recipients (phone numbers in E.164 format, e.g., +15551234567) and the messageBody
  • Input Validation: Checks if the recipients array is empty and throws a BadRequestException if it is
  • Binding Formatting: Maps recipient phone numbers into the JSON structure required by the toBinding parameter ({ "binding_type": "sms", "address": number })
  • API Call: Uses the initialized twilioClient to call client.notify.services(SERVICE_SID).notifications.create()
    • toBinding – Formatted array of recipient bindings
    • body – 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 error
    • 429 – Rate limit exceeded
    • 21211/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.

typescript
// 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:

DecoratorPurpose
@IsArray, @ArrayNotEmptyEnsures 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, @IsNotEmptyEnsures 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.

typescript
// 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 Logger for 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: SendBulkSmsDto binds the incoming JSON request body to the DTO
  • ValidationPipe automatically validates the object – validation failures return 400 Bad Request
  • Logs the incoming request with recipient count
  • Destructures validated recipients and message from 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:

bash
npm install @nestjs/swagger swagger-ui-express
# or
yarn add @nestjs/swagger swagger-ui-express

Configure it in src/main.ts:

typescript
// 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:

  1. Store Recipient Lists – Manage groups of users
  2. Log Message Status – Track broadcast status (e.g., initiated, completed, failed)

Install Prisma

bash
npm install prisma @prisma/client --save-dev
# or
yarn add prisma @prisma/client -D

Initialize Prisma

bash
npx prisma init --datasource-provider postgresql # or your chosen DB

This creates a prisma directory with schema.prisma and updates .env with DATABASE_URL. Configure your DATABASE_URL in .env.

Define Schema

prisma
// 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

bash
npx prisma migrate dev --name init-messaging-schema

Generate Prisma Client

bash
npx prisma generate

Create Prisma Service

Abstract database interactions into a service.

bash
nest generate service prisma --no-spec
typescript
// 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 BulkMessageLog after initiating the send
typescript
// 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 TypeHandling MethodHTTP Status
Validation ErrorsValidationPipe (automatic)400 Bad Request
Empty Recipient ErrorsMessagingService throws BadRequestException400 Bad Request
Twilio API Errorstry...catch block in MessagingService checks specific error.code and error.status values400, 429, 500
Configuration ErrorsConstructor check in MessagingService on startup500 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:

LocationEventLog Level
MessagingControllerIncoming requestsinfo
MessagingService constructorConfiguration loading/validationinfo/error
MessagingServiceInitiation of bulk send with recipient countinfo
MessagingServiceSuccessful notification creation with SIDinfo
MessagingServiceErrors during Twilio API calls with detailserror
Database operationsLogging successes/failuresinfo/error
MessagingServiceEmpty recipient list warningswarn

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:

  1. 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.
  2. Client-Side Retry – The client calling your API can implement retries if they receive a 5xx or 429 response.
  3. 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.

typescript
// 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.sendBulkSms

For 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:

  1. Implement Authentication & Authorization – Add JWT-based authentication with @nestjs/passport and role-based access control to restrict bulk messaging to authorized users
  2. Set Up Status Webhooks – Configure Twilio callback URLs to receive delivery receipts and update your database with real-time message status
  3. Add Recipient List Management – Build CRUD endpoints for managing recipient groups, importing CSV files, and handling opt-outs with proper consent tracking
  4. Integrate Job Queues – Implement BullMQ or RabbitMQ to handle large broadcast jobs asynchronously with retry logic and progress tracking
  5. Implement Rate Limiting – Add @nestjs/throttler to protect your API from abuse and prevent excessive Twilio charges
  6. Set Up Monitoring & Alerts – Integrate Datadog, Sentry, or New Relic to track API performance, error rates, and Twilio spending
  7. Add Message Scheduling – Allow users to schedule broadcasts for future delivery with timezone support using job queues with delayed execution
  8. Build Analytics Dashboard – Track metrics like delivery rates, failed messages, recipient engagement, and cost per message

Additional Resources

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.