code examples

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

Developer Guide: Implementing Bulk SMS Messaging with NestJS and AWS SNS

A comprehensive guide on building a system to send bulk SMS messages using NestJS and AWS Simple Notification Service (SNS), covering setup, implementation, error handling, and optional database logging.

This guide provides a complete walkthrough for building a robust system capable of sending bulk SMS messages using NestJS and AWS Simple Notification Service (SNS). We will cover everything from initial project setup to deployment and monitoring, enabling you to reliably send SMS messages at scale directly from your application.

This implementation addresses the need for programmatic, high-throughput SMS communication, often required for notifications, alerts, marketing campaigns, or multi-factor authentication. We chose NestJS for its structured, scalable architecture based on TypeScript and Node.js, and AWS SNS for its proven reliability, scalability, and cost-effectiveness in delivering SMS messages globally. By the end of this guide, you will have a functional API endpoint capable of accepting a list of phone numbers and a message, sending SMS to each recipient via SNS, and handling results and potential errors gracefully.

System Architecture:

+-------------+ +---------------------+ +-----------+ +-----------------+ | Client | ----> | NestJS API Server | ----> | AWS SNS | ----> | User Mobile Phones| | (Web/Mobile)| | (Bulk SMS Endpoint) | | | | (SMS Recipients)| +-------------+ +---------------------+ +-----------+ +-----------------+ | | | | HTTP Request | AWS SDK Call | SMS Delivery | (POST /sms/bulk-send) | (PublishCommand) | | | | v v v +-------------+ +---------------------+ +-----------------+ | Request Body| | SnsService | | CloudWatch Logs | | {numbers,msg}| | (Handles iteration | | & Metrics | +-------------+ | & individual sends) | +-----------------+ +---------------------+

Prerequisites:

  • An active AWS account with permissions to manage SNS and IAM.
  • AWS Access Key ID and Secret Access Key configured for programmatic access. See AWS Docs: Managing access keys
  • Node.js (LTS version recommended) and npm/yarn installed.
  • Basic understanding of NestJS concepts (modules, controllers, services). See NestJS Docs
  • Familiarity with TypeScript.

1. Project Setup

First, we establish the foundation for our NestJS application, installing necessary dependencies and configuring the environment.

  1. Install NestJS CLI: If you don't have it, install the NestJS command-line interface globally.

    bash
    npm install -g @nestjs/cli
  2. Create New NestJS Project: Generate a new NestJS project.

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

    Choose your preferred package manager (npm or yarn) when prompted.

  3. Install Dependencies: We need the AWS SDK v3 for SNS, configuration management, and validation.

    bash
    npm install @nestjs/config @aws-sdk/client-sns class-validator class-transformer

    Or using yarn:

    bash
    yarn add @nestjs/config @aws-sdk/client-sns class-validator class-transformer
    • @nestjs/config: Handles environment variables securely.
    • @aws-sdk/client-sns: The official AWS SDK v3 module for interacting with SNS.
    • class-validator & class-transformer: Used for validating incoming request data (DTOs).
  4. Configure Environment Variables: Create a .env file in the project root to store AWS credentials and configuration. Never commit this file to version control.

    dotenv
    # .env
    # --- AWS Credentials ---
    # Replace these placeholder values with your actual AWS credentials.
    AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
    AWS_REGION=us-east-1 # Or your preferred AWS region
    
    # --- SNS Configuration (Optional) ---
    # Default SNS message type (Transactional for high reliability, Promotional for lower cost)
    # See: https://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html#sms_publish_sdk
    SNS_DEFAULT_SENDER_ID=MyCompany # Optional: Custom Sender ID (requires registration in some countries)
    SNS_DEFAULT_SMS_TYPE=Transactional # Or Promotional
    • Important: Replace YOUR_AWS_ACCESS_KEY_ID and YOUR_AWS_SECRET_ACCESS_KEY with your actual AWS credentials.
    • Choose the AWS region where you want to operate SNS. SMS sending availability and pricing vary by region.
  5. Load Environment Variables Globally: Modify src/app.module.ts to load the .env file and make configuration accessible throughout the application.

    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 { SmsModule } from './sms/sms.module'; // We will create this next
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true, // Make ConfigModule globally available
          envFilePath: '.env', // Specify the env file
        }),
        SmsModule, // Import the module handling SMS logic
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
  6. Enable Global Validation Pipe: Configure automatic validation for incoming request bodies using DTOs. Modify src/main.ts.

    typescript
    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ValidationPipe } from '@nestjs/common'; // Import ValidationPipe
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
    
      // Enable global validation pipe
      app.useGlobalPipes(new ValidationPipe({
        whitelist: true, // Strip properties not defined in DTO
        transform: true, // Automatically transform payloads to DTO instances
      }));
    
      await app.listen(3000);
      console.log(`Application is running on: ${await app.getUrl()}`);
    }
    bootstrap();
  7. Project Structure: We'll organize our SMS logic within a dedicated sms module. Create the necessary folders:

    bash
    mkdir src/sms
    mkdir src/sms/dto

2. Implementing Core Functionality: The SNS Service

The SnsService will encapsulate the logic for interacting with AWS SNS, including initializing the client and sending messages.

  1. Create the SNS Service File:

    bash
    touch src/sms/sns.service.ts
  2. Implement the SnsService:

    typescript
    // src/sms/sns.service.ts
    import { Injectable, Logger } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import {
      SNSClient,
      PublishCommand,
      PublishCommandInput,
      PublishCommandOutput,
      SetSMSAttributesCommand, // Import command for setting attributes
    } from '@aws-sdk/client-sns';
    
    @Injectable()
    export class SnsService {
      private readonly logger = new Logger(SnsService.name);
      private readonly snsClient: SNSClient;
      private readonly defaultSmsType: string;
      private readonly defaultSenderId?: string;
    
      constructor(private configService: ConfigService) {
        const region = this.configService.get<string>('AWS_REGION');
        const accessKeyId = this.configService.get<string>('AWS_ACCESS_KEY_ID');
        const secretAccessKey = this.configService.get<string>(
          'AWS_SECRET_ACCESS_KEY',
        );
    
        if (!region || !accessKeyId || !secretAccessKey || accessKeyId === 'YOUR_AWS_ACCESS_KEY_ID' || secretAccessKey === 'YOUR_AWS_SECRET_ACCESS_KEY') {
          throw new Error('AWS credentials or region not configured properly in .env. Ensure placeholders are replaced.');
        }
    
        this.snsClient = new SNSClient({
          region: region,
          credentials: {
            accessKeyId: accessKeyId,
            secretAccessKey: secretAccessKey,
          },
        });
    
        this.defaultSmsType = this.configService.get<string>(
          'SNS_DEFAULT_SMS_TYPE',
          'Transactional', // Default to Transactional if not set
        );
        this.defaultSenderId = this.configService.get<string>(
          'SNS_DEFAULT_SENDER_ID',
        );
    
        this.logger.log(
          `SNS Service initialized. Region: ${region}, Default SMS Type: ${this.defaultSmsType}`,
        );
        // Optional: Set account-level SMS attributes on initialization if needed
        // this.setAccountSmsAttributes();
      }
    
      // Optional: Method to set global SMS attributes like DefaultSMSType
      // Usually set once via AWS Console or CLI, but can be done programmatically.
      // async setAccountSmsAttributes() {
      //   try {
      //     const command = new SetSMSAttributesCommand({
      //       attributes: {
      //         DefaultSMSType: this.defaultSmsType,
      //         // Add other attributes like UsageReportS3Bucket etc. if needed
      //       },
      //     });
      //     await this.snsClient.send(command);
      //     this.logger.log(`Successfully set default SMS type to ${this.defaultSmsType}`);
      //   } catch (error) {
      //     this.logger.error('Failed to set account SMS attributes', error.stack);
      //   }
      // }
    
      /**
       * Sends SMS messages to multiple phone numbers using AWS SNS.
       * Handles each send operation individually and returns a summary of results.
       * @param phoneNumbers Array of phone numbers in E.164 format (e.g., +12223334444).
       * @param message The text message content.
       * @returns A summary object with counts and lists of successful/failed sends.
       */
      async sendBulkSms(phoneNumbers: string[], message: string) {
        this.logger.log(
          `Attempting to send SMS to ${phoneNumbers.length} numbers.`,
        );
    
        const results = await Promise.allSettled(
          phoneNumbers.map((phoneNumber) => this.sendSingleSms(phoneNumber, message)),
        );
    
        const successfulSends: { phoneNumber: string; messageId: string }[] = [];
        const failedSends: { phoneNumber: string; error: string }[] = [];
    
        results.forEach((result, index) => {
          const phoneNumber = phoneNumbers[index];
          if (result.status === 'fulfilled') {
            successfulSends.push({ phoneNumber, messageId: result.value.MessageId });
            this.logger.log(`Successfully sent SMS to ${phoneNumber}`);
          } else {
            const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
            failedSends.push({ phoneNumber, error: errorMessage });
            this.logger.error(
              `Failed to send SMS to ${phoneNumber}: ${errorMessage}`,
              result.reason instanceof Error ? result.reason.stack : undefined,
            );
          }
        });
    
        this.logger.log(
          `Bulk SMS send complete. Success: ${successfulSends.length}, Failed: ${failedSends.length}`,
        );
    
        return {
          successCount: successfulSends.length,
          failCount: failedSends.length,
          successful: successfulSends,
          failed: failedSends,
        };
      }
    
      /**
       * Sends a single SMS message using AWS SNS.
       * @param phoneNumber The recipient phone number in E.164 format.
       * @param message The text message content.
       * @returns The PublishCommandOutput from AWS SNS upon success.
       */
      private async sendSingleSms(
        phoneNumber: string,
        message: string,
      ): Promise<PublishCommandOutput> {
        const params: PublishCommandInput = {
          PhoneNumber: phoneNumber,
          Message: message,
          MessageAttributes: {
            'AWS.SNS.SMS.SMSType': {
              DataType: 'String',
              StringValue: this.defaultSmsType, // Use Transactional or Promotional
            },
            // Optionally add SenderID if configured and supported
            ...(this.defaultSenderId && {
              'AWS.SNS.SMS.SenderID': {
                DataType: 'String',
                StringValue: this.defaultSenderId,
              }
            }),
          },
        };
    
        const command = new PublishCommand(params);
    
        try {
          const data = await this.snsClient.send(command);
          // this.logger.debug(`SNS Publish Response for ${phoneNumber}: ${JSON.stringify(data)}`);
          if (!data.MessageId) {
             throw new Error('SNS did not return a MessageId');
          }
          return data;
        } catch (error) {
          // Log the detailed error but re-throw to be caught by Promise.allSettled
          this.logger.error(`SNS Publish error for ${phoneNumber}: ${error.message}`, error.stack);
          throw error; // Re-throw the error
        }
      }
    }
    • SNSClient Initialization: We create an instance of the SNSClient using credentials and region from ConfigService. An error is thrown if essential variables are missing or still contain placeholder values.
    • sendBulkSms Method:
      • Accepts an array of phoneNumbers and the message.
      • Uses Promise.allSettled to send messages concurrently. This is crucial because it allows individual sends to fail without stopping the entire batch, unlike Promise.all.
      • Calls sendSingleSms for each number.
      • Iterates through the results array (Promise.allSettled returns an array of objects describing the outcome of each promise: { status: 'fulfilled', value: ... } or { status: 'rejected', reason: ... }).
      • Builds successfulSends and failedSends arrays based on the outcomes.
      • Logs success or failure for each number.
      • Returns a summary object containing counts and lists of successful and failed sends, including MessageIDs for successes and error reasons for failures.
    • sendSingleSms Method:
      • Constructs the PublishCommandInput including the PhoneNumber, Message, and crucial MessageAttributes.
      • AWS.SNS.SMS.SMSType: Set to Transactional (default, optimizes for reliability) or Promotional (optimizes for cost) based on the .env variable.
      • AWS.SNS.SMS.SenderID: Optionally included if SNS_DEFAULT_SENDER_ID is set in .env. Note that Sender ID support and registration requirements vary by country. See AWS Docs: Sender ID
      • Sends the command using this.snsClient.send().
      • Includes basic logging and error handling, re-throwing the error to be caught by sendBulkSms.

3. Building the API Layer

We need a controller and a Data Transfer Object (DTO) to handle incoming API requests for sending bulk SMS.

  1. Create the DTO File:

    bash
    touch src/sms/dto/send-bulk-sms.dto.ts
  2. Define the SendBulkSmsDto: Use class-validator decorators to enforce input validation.

    typescript
    // src/sms/dto/send-bulk-sms.dto.ts
    import {
      IsArray,
      IsString,
      IsNotEmpty,
      ArrayNotEmpty,
      Matches,
      MaxLength,
    } from 'class-validator';
    
    export class SendBulkSmsDto {
      @IsArray()
      @ArrayNotEmpty()
      @IsString({ each: true }) // Ensure each element in the array is a string
      @Matches(/^\+[1-9]\d{1,14}$/, { // Basic E.164 format regex validation
        each: true,
        message: 'Each phone number must be in E.164 format (e.g., +12223334444)',
      })
      phoneNumbers: string[];
    
      @IsString()
      @IsNotEmpty()
      @MaxLength(1600) // SNS message length limit (check current limits)
      message: string;
    }
    • phoneNumbers: Must be a non-empty array of strings, and each string must conform to a basic E.164 format regex.
    • message: Must be a non-empty string with a maximum length (SNS has limits, usually around 1600 bytes, but SMS segments are smaller – see Caveats).
  3. Create the Controller File:

    bash
    touch src/sms/sms.controller.ts
  4. Implement the SmsController:

    typescript
    // src/sms/sms.controller.ts
    import { Controller, Post, Body, HttpCode, HttpStatus, Logger } from '@nestjs/common';
    import { SnsService } from './sns.service';
    import { SendBulkSmsDto } from './dto/send-bulk-sms.dto';
    
    @Controller('sms')
    export class SmsController {
      private readonly logger = new Logger(SmsController.name);
    
      constructor(private readonly snsService: SnsService) {}
    
      @Post('bulk-send')
      @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as processing is asynchronous
      async sendBulkSms(@Body() sendBulkSmsDto: SendBulkSmsDto) {
        this.logger.log(`Received bulk SMS request for ${sendBulkSmsDto.phoneNumbers.length} numbers.`);
        // No explicit try-catch needed here if we want NestJS default exception filter
        // to handle errors from SnsService (like configuration issues)
        const result = await this.snsService.sendBulkSms(
          sendBulkSmsDto.phoneNumbers,
          sendBulkSmsDto.message,
        );
    
        // Decide on response structure - maybe return Accepted and log details,
        // or return the summary if the client needs immediate feedback.
        // Returning summary here for clarity.
        return result;
      }
    }
    • Defines a route POST /sms/bulk-send.
    • Injects SnsService.
    • Uses the SendBulkSmsDto with the @Body() decorator, triggering validation via the global ValidationPipe.
    • Sets HttpCode to 202 Accepted – this is appropriate as the request is accepted, but individual message delivery happens asynchronously and might fail later.
    • Calls snsService.sendBulkSms with validated data.
    • Returns the result summary from the service.
  5. Create and Configure the SmsModule: Tie the controller and service together.

    bash
    touch src/sms/sms.module.ts
    typescript
    // src/sms/sms.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config'; // Import ConfigModule if service needs it
    import { SnsService } from './sns.service';
    import { SmsController } from './sms.controller';
    
    @Module({
      imports: [ConfigModule], // SnsService depends on ConfigService
      controllers: [SmsController],
      providers: [SnsService],
    })
    export class SmsModule {}
    • Ensure this module is imported in src/app.module.ts (as done in Step 1.5).

Testing the Endpoint:

You can now run the application (npm run start:dev) and test the endpoint using curl or a tool like Postman:

bash
# Note: Ensure your shell correctly handles the quotes within the JSON data.
curl -X POST http://localhost:3000/sms/bulk-send \
-H "Content-Type: application/json" \
-d '{
  "phoneNumbers": ["+15551112222", "+15553334444", "+invalidNumber"],
  "message": "Hello from NestJS SNS Bulk Sender!"
}'

Expected JSON Request:

json
{
  "phoneNumbers": ["+15551112222", "+15553334444"],
  "message": "This is a test message."
}

Example JSON Response (Success):

json
{
  "successCount": 2,
  "failCount": 0,
  "successful": [
    { "phoneNumber": "+15551112222", "messageId": "uuid-for-sms-1" },
    { "phoneNumber": "+15553334444", "messageId": "uuid-for-sms-2" }
  ],
  "failed": []
}

Example JSON Response (Partial Failure):

json
{
    "successCount": 1,
    "failCount": 1,
    "successful": [
        {
            "phoneNumber": "+15551112222",
            "messageId": "b1a2b3c4-abcd-efgh-ijkl-1234567890ab"
        }
    ],
    "failed": [
        {
            "phoneNumber": "+19998887777",
            "error": "Invalid phone number format or destination is opted out"
        }
    ]
}

4. Integrating with AWS SNS

This section focuses on the specific integration points covered in the SnsService setup.

  1. AWS Credentials:

    • Obtaining Keys: Generate an Access Key ID and Secret Access Key for an IAM user in the AWS Management Console. Go to IAM > Users > [Your User] > Security credentials > Create access key.
    • Secure Storage: Store these keys securely in the .env file in your project root. Do not hardcode them in your source code. Use environment variables in production environments (e.g., set via ECS Task Definitions, Lambda environment variables, EC2 instance profiles, or secrets management services like AWS Secrets Manager). Remember to replace the placeholder values in the .env file.
    • Permissions: Ensure the IAM user associated with the keys has the necessary permissions. A minimal policy would be:
      json
      {
          ""Version"": ""2012-10-17"",
          ""Statement"": [
              {
                  ""Effect"": ""Allow"",
                  ""Action"": ""sns:Publish"",
                  ""Resource"": ""*""
              }
          ]
      }
      Optionally add permissions for sns:SetSMSAttributes or sns:CheckIfPhoneNumberIsOptedOut if needed, restricting the Resource if possible. Apply the principle of least privilege.
  2. Configuration (.env):

    • AWS_ACCESS_KEY_ID: Your IAM user's access key ID (must be replaced).
    • AWS_SECRET_ACCESS_KEY: Your IAM user's secret access key (must be replaced).
    • AWS_REGION: The AWS region where SNS will operate (e.g., us-east-1, eu-west-1). Must match the region configured in the SNSClient.
    • SNS_DEFAULT_SMS_TYPE: (Optional, defaults to Transactional) Sets the AWS.SNS.SMS.SMSType message attribute. Transactional prioritizes delivery speed and reliability, while Promotional prioritizes lower cost. Choose based on your use case.
    • SNS_DEFAULT_SENDER_ID: (Optional) A custom ID (alphanumeric, up to 11 chars; or a phone number you own) displayed on the recipient's device. Support varies by country, and registration might be required. See AWS Docs
  3. SNS Client Initialization (SnsService): The SNSClient from @aws-sdk/client-sns is instantiated with the region and credentials loaded via ConfigService. This ensures the SDK communicates with the correct AWS region and authenticates properly.

  4. Fallback Mechanisms: AWS SNS is highly available, but consider application-level resilience:

    • Retries: Implement retries for transient errors (see Section 5).
    • Cross-Region Failover: For critical systems, you could potentially configure a secondary SNSClient instance pointing to a different region and failover if the primary region experiences issues (adds complexity).
    • Alternative Providers: In extreme cases, integrating with a second SMS provider as a fallback could be considered, but significantly increases complexity.

5. Error Handling, Logging, and Retry Mechanisms

Robust error handling and logging are essential for a production system.

  1. Consistent Error Handling Strategy:

    • Service Level: The SnsService catches errors during individual snsClient.send() calls within the sendSingleSms method. It logs the specific error and re-throws it. Promise.allSettled in sendBulkSms captures these rejections without halting the loop, and the results are processed to build the final summary.
    • Controller Level: The SmsController relies on NestJS's built-in exception filters for unhandled errors (e.g., configuration issues during service initialization). Validation errors are handled globally by the ValidationPipe.
    • Logging: Use the built-in Logger (@nestjs/common) for structured logging. Log key events: request received, bulk send initiated, individual send success/failure (with error details), bulk send completion summary.
  2. Logging Levels and Formats:

    • Use this.logger.log() for informational messages.
    • Use this.logger.error() for errors, including the error message and stack trace (error.stack).
    • Use this.logger.warn() for potential issues (e.g., high failure rate).
    • Use this.logger.debug() for verbose information useful during development (e.g., full SNS responses).
    • Consider using a dedicated logging library (like pino with nestjs-pino) in production for better performance and structured JSON logging, which integrates well with log aggregation services (CloudWatch Logs, Datadog, Splunk).
  3. Retry Strategies:

    • SNS Internal Retries: SNS itself has some internal retry mechanisms for transient delivery issues.
    • Application-Level Retries: The current implementation doesn't include explicit application-level retries for the PublishCommand. For transient AWS SDK errors (e.g., network timeouts, throttling), you could add a simple retry loop within sendSingleSms:
      typescript
      // Inside sendSingleSms, before the catch block
      let attempts = 0;
      const maxAttempts = 3;
      while (attempts < maxAttempts) {
          try {
              const data = await this.snsClient.send(command);
              if (!data.MessageId) {
                  throw new Error('SNS did not return a MessageId');
              }
              return data; // Success, exit loop
          } catch (error) {
              attempts++;
              const isRetryable = error.name === 'ThrottlingException' || error.name === 'EndpointConnectionError'; // Add other retryable error names
              if (isRetryable && attempts < maxAttempts) {
                  const delay = Math.pow(2, attempts) * 100; // Exponential backoff
                  this.logger.warn(`Attempt ${attempts} failed for ${phoneNumber}, retrying in ${delay}ms... Error: ${error.message}`);
                  await new Promise(resolve => setTimeout(resolve, delay));
              } else {
                   if (isRetryable) { // Log max attempts reached only for retryable errors
                       this.logger.warn(`SNS Publish failed after ${maxAttempts} attempts for ${phoneNumber}: ${error.message}`);
                   }
                  // Non-retryable error or max attempts reached
                  throw error;
              }
          }
      }
      // Should not be reached if maxAttempts > 0, but needed for TS/logic completion
      throw new Error(`Retry logic failed unexpectedly after ${maxAttempts} attempts for ${phoneNumber}.`);
      This adds complexity; evaluate if necessary based on observed errors.
    • Queue-Based Retries (Advanced): For maximum robustness, especially with very large batches or frequent transient errors, decouple sending. The API endpoint could push each SMS job (phone number + message) onto an SQS queue. A separate NestJS worker process would consume jobs from the queue, attempt to send via SNS, and implement sophisticated retry logic (including dead-letter queues for persistent failures). This prevents API timeouts and handles failures more gracefully.
  4. Testing Error Scenarios:

    • Provide invalid E.164 numbers in the request.
    • Provide a message exceeding length limits.
    • Temporarily revoke sns:Publish permissions from the IAM user to test authorization errors.
    • Send to known opted-out numbers (if available for testing).
    • Simulate throttling (hard to do reliably without high volume, but be aware of the ThrottlingException).
    • Unit test error paths by mocking snsClient.send() to throw specific errors.
  5. Log Analysis: Use CloudWatch Logs Insights or your chosen log aggregation tool to query logs. Examples:

    • Find all failed sends: fields @timestamp, @message | filter @message like /Failed to send SMS/
    • Count errors per phone number: fields @timestamp, phoneNumber, error | filter @message like /Failed to send SMS to/ | parse @message /Failed to send SMS to (?<phoneNumber>\+\S+) *: (?<error>.*)/ | stats count(*) by phoneNumber, error
    • Monitor overall success/failure rates: fields successCount, failCount | filter @message like /Bulk SMS send complete/ | parse @message /Success: (?<successCount>\d+), Failed: (?<failCount>\d+)/ | stats sum(successCount), sum(failCount)

6. Creating a Database Schema and Data Layer (Optional Enhancement)

While not strictly necessary for sending, storing logs of sent messages provides valuable auditing, tracking, and debugging capabilities.

  1. Rationale: A database log allows you to:

    • Audit exactly which messages were sent to whom and when.
    • Track the AWS SNS MessageId for correlation with delivery status reports (if configured).
    • Analyze send success/failure trends over time.
    • Provide users with a history of sent messages.
  2. Technology Choice: PostgreSQL, MySQL, or even a NoSQL database like DynamoDB could be used. For simplicity, we'll outline a TypeORM setup with SQLite (suitable for development/small scale). For production, use PostgreSQL or MySQL.

  3. Install Dependencies (if not already present):

    bash
    npm install @nestjs/typeorm typeorm sqlite3 # Or pg, mysql2 etc. for other DBs

    Or using yarn:

    bash
    yarn add @nestjs/typeorm typeorm sqlite3
  4. Entity Relationship Diagram (Simple):

    +--------------------+ | SmsLog | +--------------------+ | id (PK) | --> Auto-incrementing ID (or UUID) | phoneNumber | --> Recipient phone number (string, indexed) | messageContent | --> The message text (text) | status | --> 'SUCCESS' | 'FAILED' (string, indexed) | snsMessageId (nullable) | --> AWS SNS Message ID (string, indexed, unique if desired) | failureReason (nullable) | --> Error message if status is 'FAILED' (text) | createdAt | --> Timestamp (indexed) +--------------------+
  5. Create SmsLog Entity:

    typescript
    // src/sms/entities/sms-log.entity.ts
    import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
    
    export enum SmsStatus {
      SUCCESS = 'SUCCESS',
      FAILED = 'FAILED',
    }
    
    @Entity('sms_logs')
    export class SmsLog {
      @PrimaryGeneratedColumn('uuid') // Use UUID for better distribution
      id: string;
    
      @Index()
      @Column()
      phoneNumber: string;
    
      @Column('text')
      messageContent: string;
    
      @Index()
      @Column({
        type: 'enum',
        enum: SmsStatus,
      })
      status: SmsStatus;
    
      @Index() // Index for looking up by SNS ID
      @Column({ nullable: true, unique: false }) // Can be null if send failed before getting ID. Not strictly unique across retries?
      snsMessageId?: string;
    
      @Column('text', { nullable: true })
      failureReason?: string;
    
      @CreateDateColumn()
      @Index()
      createdAt: Date;
    }
  6. Configure TypeORM: Update src/app.module.ts (replace SQLite with your production DB config).

    typescript
    // src/app.module.ts (add TypeOrmModule imports)
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { SmsLog } from './sms/entities/sms-log.entity';
    // ... other imports
    
    @Module({
      imports: [
        ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
        TypeOrmModule.forRoot({ // Replace with your DB config
          type: 'sqlite',
          database: 'sms_logs.db',
          entities: [SmsLog],
          synchronize: true, // DEV only! Use migrations in production
        }),
        // Make S

Frequently Asked Questions

How to send bulk SMS with NestJS?

You can send bulk SMS messages by creating a NestJS application that integrates with AWS SNS. The application uses the AWS SDK to interact with the SNS service, sending messages to specified phone numbers. You'll need an AWS account and configure credentials within your NestJS project.

What is AWS SNS used for in bulk SMS?

AWS SNS (Simple Notification Service) is used as the messaging platform to deliver SMS messages to recipients. It handles the complexities of sending messages reliably at scale. The NestJS application acts as an interface to interact with SNS.

Why use NestJS for sending bulk SMS?

NestJS provides a structured and scalable architecture based on TypeScript and Node.js. This makes it well-suited for building robust applications capable of handling the demands of bulk SMS messaging.

How to set up AWS credentials for NestJS SMS?

Create an IAM user in your AWS account with permissions to use SNS, generate access keys, and store them securely in a `.env` file in your project's root directory. Never commit this file to version control.

What is the role of a DTO in NestJS bulk SMS?

A Data Transfer Object (DTO) defines the structure of incoming requests. In this case, the `SendBulkSmsDto` ensures that the API receives valid phone numbers and message content, using validation decorators from `class-validator`.

How to handle errors when sending bulk SMS?

The provided code uses `Promise.allSettled` to handle individual SMS failures without stopping the entire batch. Logging is implemented using NestJS's built-in `Logger`, and the code includes a detailed explanation of how to implement retry mechanisms for transient errors.

What is the purpose of the SnsService in NestJS?

The `SnsService` encapsulates the logic for interacting with AWS SNS. It initializes the SNS client, sets default message attributes, and provides methods to send single and bulk SMS messages. It also handles errors and provides a summary of sent messages.

How to structure a bulk SMS API endpoint in NestJS?

Create a controller with a POST route that accepts a `SendBulkSmsDto` object. This DTO should contain an array of phone numbers and the message content. The controller then uses the `SnsService` to send the messages.

What is the recommended HTTP status code for bulk SMS sending?

Use HTTP status code 202 (Accepted), as the actual SMS delivery is asynchronous. This acknowledges that the request has been received and is being processed, even if individual messages fail later.

How to validate phone numbers in a NestJS bulk SMS API?

Use the `class-validator` library. The `SendBulkSmsDto` demonstrates how to validate phone numbers using decorators like `@IsArray`, `@ArrayNotEmpty`, `@IsString`, and a regex `@Matches` to enforce E.164 format.

What are the prerequisites for implementing bulk SMS with NestJS and AWS SNS?

You'll need an AWS account with SNS permissions, AWS access keys, Node.js and npm/yarn, a basic understanding of NestJS and TypeScript, and optionally, a database for logging.

When should I use Transactional vs. Promotional SMS type?

Use 'Transactional' for critical messages requiring high reliability, like one-time passwords. 'Promotional' is suitable for marketing messages where lower cost is preferred. This is configurable in the '.env' file and used by the `SnsService`.

Can I customize the sender ID for bulk SMS messages?

Yes, you can set `SNS_DEFAULT_SENDER_ID` in your `.env` file. However, support for custom sender IDs varies by country and may require registration with AWS. See AWS documentation for details.

How to log sent SMS messages for auditing and analysis?

The article suggests using a database to log sent messages, including details like phone number, message content, status, SNS message ID, and errors. It outlines a TypeORM setup with SQLite as an example.