code examples

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

Sending SMS with AWS SNS in NestJS: A Developer Guide

A step-by-step guide for integrating AWS SNS into a NestJS application to send SMS messages, covering setup, implementation, security, and testing.

This guide provides a step-by-step walkthrough for integrating AWS Simple Notification Service (SNS) into a NestJS application to send SMS messages directly to phone numbers. We'll cover everything from project setup and AWS configuration to implementation, error handling, security, and testing.

By the end of this tutorial, you'll have a functional NestJS API endpoint capable of accepting a phone number and message, and using AWS SNS to deliver that message as an SMS.

Project Overview and Goals

What we're building: A simple NestJS application with a single API endpoint (POST /sms/send) that accepts a phone number and a message body, then uses AWS SNS to send the message as an SMS.

Problem solved: Provides a robust, scalable, and cloud-native way to programmatically send SMS messages (like OTPs, notifications, alerts) from your NestJS backend without managing complex telephony infrastructure.

Technologies used:

  • NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript. Chosen for its modular architecture, dependency injection, and built-in tooling.
  • AWS SNS: A fully managed messaging service for both application-to-application (A2A) and application-to-person (A2P) communication. Chosen for its direct SMS sending capability, scalability, reliability, and integration with the AWS ecosystem.
  • AWS SDK for JavaScript v3: The official AWS SDK for interacting with AWS services, including SNS, from Node.js/TypeScript applications.
  • TypeScript: Superset of JavaScript adding static types, enhancing code quality and maintainability.
  • dotenv / @nestjs/config: For managing environment variables securely.

System Architecture:

mermaid
graph LR
    Client[Client Application / Postman] -- HTTP POST Request --> API{NestJS API Endpoint (/sms/send)}
    API -- Uses --> SmsService{SmsService (NestJS)}
    SmsService -- Reads Config --> Config{Environment Variables (.env)}
    SmsService -- Calls AWS SDK --> SNSClient[AWS SNS Client (@aws-sdk/client-sns)]
    SNSClient -- Sends SMS Request --> AWS_SNS[AWS SNS Service]
    AWS_SNS -- Delivers SMS --> Phone[(User's Phone)]
    Config -- Stores --> AWSCreds[AWS Credentials & Region]

Prerequisites:

  • Node.js (LTS version recommended, check AWS SDK v3 requirements)
  • npm or yarn package manager
  • An active AWS account
  • AWS Access Key ID and Secret Access Key with permissions to use SNS (we'll cover creating this)
  • A text editor or IDE (like VS Code)
  • Basic understanding of TypeScript, NestJS, and REST APIs
  • AWS CLI (Optional, but helpful for configuration and testing)

1. Setting up the NestJS Project

Let's start by creating a new NestJS project and installing the necessary dependencies.

  1. Create a new NestJS project: Open your terminal and run the Nest CLI command:

    bash
    npx @nestjs/cli new nestjs-sns-sms
    cd nestjs-sns-sms

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

  2. Install required dependencies: We need the AWS SDK v3 client for SNS, NestJS config module for environment variables, and class-validator/class-transformer for input validation.

    bash
    # Using npm
    npm install @aws-sdk/client-sns @nestjs/config class-validator class-transformer
    
    # Using yarn
    yarn add @aws-sdk/client-sns @nestjs/config class-validator class-transformer
    • @aws-sdk/client-sns: The modular AWS SDK v3 package specifically for SNS interactions.
    • @nestjs/config: Handles environment variable loading and access in a structured way.
    • class-validator & class-transformer: Used for validating incoming request data (DTOs).
  3. Configure Environment Variables: NestJS encourages using a .env file for environment-specific configurations, especially sensitive data like AWS credentials.

    • Create a .env file in the project root (nestjs-sns-sms/.env):

      dotenv
      # .env
      
      # AWS Credentials - Obtain from IAM User setup
      AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
      AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
      
      # AWS Region - Choose one that supports SMS, e.g., us-east-1
      AWS_REGION=us-east-1
      
      # Optional: Default SMS Type (Transactional or Promotional)
      # Transactional is better for OTPs/critical alerts, may bypass DND
      # Promotional is cheaper, better for marketing
      AWS_SNS_DEFAULT_SMS_TYPE=Transactional

      Important: Replace YOUR_AWS_ACCESS_KEY_ID and YOUR_AWS_SECRET_ACCESS_KEY with the actual credentials you'll generate in the next step. Add this .env file to your .gitignore to prevent committing secrets.

    • Load the configuration module in src/app.module.ts:

      typescript
      // src/app.module.ts
      import { Module } from '@nestjs/common';
      import { AppController } from './app.controller';
      import { AppService } from './app.service';
      import { ConfigModule } from '@nestjs/config'; // Import ConfigModule
      import { SmsModule } from './sms/sms.module'; // We'll create this next
      
      @Module({
        imports: [
          ConfigModule.forRoot({
            isGlobal: true, // Make ConfigModule available globally
            envFilePath: '.env', // Specify the env file path
          }),
          SmsModule, // Import our future SMS module
        ],
        controllers: [AppController],
        providers: [AppService],
      })
      export class AppModule {}

      ConfigModule.forRoot({ isGlobal: true }) makes the ConfigService available throughout the application without needing to import ConfigModule everywhere.

2. AWS Setup: IAM User and SNS Configuration

To interact with AWS SNS securely, we need an IAM (Identity and Access Management) user with specific permissions.

  1. Create an IAM User:

    • Log in to your AWS Management Console.
    • Navigate to the IAM service.
    • In the left navigation pane, click Users, then click Create user.
    • Enter a User name (e.g., nestjs-sns-sender).
    • Select Provide user access to the AWS Management Console - Optional (Only needed if this user needs console access. For programmatic access only, leave unchecked).
    • Select I want to create an IAM user. If selected console access, choose a password method.
    • Click Next.
    • On the Set permissions page, select Attach policies directly.
    • Best Practice (Production): Click Create policy. Use the JSON editor and provide a policy granting only the necessary permissions. This adheres to the principle of least privilege. A minimal policy requires sns:Publish. Optionally, add sns:SetSMSAttributes if setting type per message or other attributes, and sns:CheckIfPhoneNumberIsOptedOut if checking opt-out status. You can restrict the Resource from "*" to specific topic ARNs if not sending directly to phones.
      json
      {
          "Version": "2012-10-17",
          "Statement": [
              {
                  "Effect": "Allow",
                  "Action": [
                      "sns:Publish",
                      "sns:SetSMSAttributes",
                      "sns:CheckIfPhoneNumberIsOptedOut"
                  ],
                  "Resource": "*"
              }
          ]
      }
      Give the policy a name (e.g., NestJsSnsSmsPublishOnly) and create it. Then, back on the "Set permissions" page for the user, search for and attach this custom policy.
    • Simpler Alternative (Guide/Testing): For simplicity during this guide or initial testing only, you can search for and select the AWS managed policy AmazonSNSFullAccess. Be aware this grants broad SNS permissions (publish, manage topics, subscriptions, etc.) and is not recommended for production.
    • Click Next.
    • Review the user details and permissions, then click Create user.
    • Crucial Step: On the success page, click on the username you just created. Navigate to the Security credentials tab. Under Access keys, click Create access key.
    • Select Application running outside AWS as the use case. Click Next.
    • Add an optional description tag. Click Create access key.
    • Immediately copy the Access key ID and Secret access key. The secret key is only shown once. Store them securely.
    • Paste these keys into your .env file for the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY variables.
  2. Choose an AWS Region: Not all AWS regions support sending SMS messages directly via SNS. Regions like us-east-1 (N. Virginia), us-west-2 (Oregon), eu-west-1 (Ireland), and ap-southeast-1 (Singapore) generally do. Check the AWS documentation for the latest list and ensure the region specified in your .env (AWS_REGION) supports SMS. us-east-1 is often a safe default choice if you don't have specific region requirements.

  3. Set Default SMS Type (Optional but Recommended): SNS allows you to optimize SMS delivery for cost (Promotional) or reliability (Transactional). Transactional messages have higher delivery priority and may bypass Do-Not-Disturb (DND) registries in some countries, making them suitable for critical alerts or OTPs.

    • You can set this default for your entire AWS account in the chosen region.
    • Navigate to the SNS service in the AWS Management Console.
    • Make sure you are in the correct Region (the one specified in your .env file).
    • In the left navigation pane, click Mobile -> Text messaging (SMS).
    • Click Edit in the "Account spending limit and default message type" section.
    • Under Default message type, select Transactional or Promotional.
    • Set a Monthly SMS spend limit (USD) to prevent unexpected costs (e.g., 1.00 for testing).
    • Click Save changes.
    • Alternatively, we can set this attribute programmatically when sending a message if needed, or rely on the AWS_SNS_DEFAULT_SMS_TYPE variable from .env if we implement reading it later. For simplicity now, setting it in the console is sufficient.

3. Implementing the SMS Service

Now, let's create the core logic for sending SMS messages within our NestJS application.

  1. Generate the SMS Module and Service: Use the Nest CLI to generate a module and service for SMS functionality.

    bash
    npx @nestjs/cli generate module sms
    npx @nestjs/cli generate service sms --no-spec # --no-spec skips test file generation for now

    This creates src/sms/sms.module.ts and src/sms/sms.service.ts. The SmsModule was already imported into AppModule earlier.

  2. Implement the SmsService: Open src/sms/sms.service.ts and add the logic to interact with AWS SNS.

    typescript
    // src/sms/sms.service.ts
    import { Injectable, Logger } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import {
      SNSClient,
      PublishCommand,
      PublishCommandInput,
      // SetSMSAttributesCommand, // Optional: If you want to set attributes per message
    } from '@aws-sdk/client-sns';
    import { AwsServiceUnavailableException } from './exceptions/aws-service-unavailable.exception'; // Custom exception
    
    @Injectable()
    export class SmsService {
      private readonly logger = new Logger(SmsService.name);
      private readonly snsClient: SNSClient;
      private readonly defaultSmsType: string;
    
      constructor(private readonly configService: ConfigService) {
        // Instantiate SNS Client during service initialization
        this.snsClient = new SNSClient({
          region: this.configService.get<string>('AWS_REGION'),
          credentials: {
            accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
            secretAccessKey: this.configService.get<string>(
              'AWS_SECRET_ACCESS_KEY',
            ),
          },
        });
        this.logger.log('AWS SNS Client Initialized');
    
        // Get default SMS type from config, default to Transactional if not set
        this.defaultSmsType = this.configService.get<string>(
          'AWS_SNS_DEFAULT_SMS_TYPE',
          'Transactional', // Default value if not in .env
        );
      }
    
      /**
       * Sends an SMS message to the specified phone number using AWS SNS.
       * Assumes phoneNumber has been validated by the controller DTO.
       * @param phoneNumber - The recipient's phone number in E.164 format (e.g., +12065550100).
       * @param message - The text message body.
       * @returns The MessageId from AWS SNS on success.
       * @throws AwsServiceUnavailableException if the SNS interaction fails.
       */
      async sendSms(phoneNumber: string, message: string): Promise<string> {
        // E.164 validation is handled by the SendSmsDto in the controller layer.
        // The service assumes it receives valid data based on DTO validation.
    
        const params: PublishCommandInput = {
          PhoneNumber: phoneNumber,
          Message: message,
          MessageAttributes: { // Optional: Set message type per message
            'AWS.SNS.SMS.SMSType': {
                DataType: 'String',
                StringValue: this.defaultSmsType // Use configured default type
            }
          }
        };
    
        try {
          this.logger.log(
            `Sending SMS to ${phoneNumber} with type ${this.defaultSmsType}`,
          );
          const command = new PublishCommand(params);
          const response = await this.snsClient.send(command);
          this.logger.log(`SMS sent successfully! Message ID: ${response.MessageId}`);
          return response.MessageId;
        } catch (error) {
          this.logger.error(`Failed to send SMS to ${phoneNumber}`, error.stack);
          // You might want to check the error type (e.g., error.name) for more specific handling
          // Examples: 'InvalidParameterException', 'AuthorizationError', etc.
          throw new AwsServiceUnavailableException(
            `Failed to send SMS via AWS SNS: ${error.message}`,
          );
        }
      }
    
      // Optional: Add methods for checking opt-out status, setting attributes etc.
      // async checkIfOptedOut(phoneNumber: string): Promise<boolean> { ... }
    }
  3. Create a Custom Exception (Optional but Good Practice): Create a file src/sms/exceptions/aws-service-unavailable.exception.ts:

    typescript
    // src/sms/exceptions/aws-service-unavailable.exception.ts
    import { HttpException, HttpStatus } from '@nestjs/common';
    
    export class AwsServiceUnavailableException extends HttpException {
      constructor(message?: string) {
        super(
          message || 'AWS Service is temporarily unavailable. Please try again later.',
          HttpStatus.SERVICE_UNAVAILABLE,
        );
      }
    }

    This helps in providing a more specific HTTP status code if the SNS service fails. We'll need an exception filter later to handle this properly, or rely on NestJS defaults for now.

  4. Ensure ConfigService is Available: Make sure ConfigModule is imported correctly in src/app.module.ts and configured as isGlobal: true. The SmsService uses @nestjs/config's ConfigService via dependency injection to securely retrieve the AWS credentials and region from the environment variables loaded from .env.

Explanation:

  • The SNSClient is initialized in the constructor using credentials and region fetched from ConfigService.
  • The sendSms method constructs the PublishCommandInput required by the AWS SDK v3.
    • PhoneNumber: Must be in E.164 format (e.g., +12223334444). Validation is now expected to happen at the API layer (DTO).
    • Message: The content of the SMS.
    • MessageAttributes (Optional): We explicitly set the AWS.SNS.SMS.SMSType attribute here based on our configuration. This ensures the message is treated as Transactional or Promotional as intended.
  • The snsClient.send() method sends the command to AWS SNS.
  • Error handling is included using a try...catch block, logging errors and throwing a custom AwsServiceUnavailableException.

4. Building the API Layer

Let's expose the SMS sending functionality through a REST API endpoint.

  1. Generate the SMS Controller:

    bash
    npx @nestjs/cli generate controller sms --no-spec

    This creates src/sms/sms.controller.ts.

  2. Create a Data Transfer Object (DTO) for Validation: Create a file src/sms/dto/send-sms.dto.ts to define the expected request body structure and apply validation rules.

    typescript
    // src/sms/dto/send-sms.dto.ts
    import { IsNotEmpty, IsString, Matches, MaxLength } from 'class-validator';
    
    export class SendSmsDto {
      @IsString()
      @IsNotEmpty()
      @Matches(/^\+[1-9]\d{1,14}$/, { // E.164 format validation
        message: 'Phone number must be in E.164 format (e.g., +12065550100)',
      })
      phoneNumber: string;
    
      @IsString()
      @IsNotEmpty()
      @MaxLength(1600) // SNS message length limits (check current limits)
      message: string;
    }
    • @IsString(), @IsNotEmpty(): Ensures the fields are non-empty strings.
    • @Matches(): Validates the phoneNumber against the E.164 regex pattern.
    • @MaxLength(): Basic check for message length (SNS has limits, typically 140 bytes for GSM-7, less for UCS-2).
  3. Implement the SmsController: Open src/sms/sms.controller.ts and define the endpoint.

    typescript
    // src/sms/sms.controller.ts
    import {
      Controller,
      Post,
      Body,
      HttpCode,
      HttpStatus,
      // ValidationPipe removed from here - rely on global pipe
      Logger,
      UsePipes, // Import if you still need ValidationPipe locally for some reason
    } from '@nestjs/common';
    import { SmsService } from './sms.service';
    import { SendSmsDto } from './dto/send-sms.dto';
    import { AwsServiceUnavailableException } from './exceptions/aws-service-unavailable.exception'; // Import custom exception
    
    @Controller('sms') // Route prefix: /sms
    export class SmsController {
      private readonly logger = new Logger(SmsController.name);
    
      constructor(private readonly smsService: SmsService) {}
    
      @Post('send') // Route: POST /sms/send
      @HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted on successful request queuing
      // @Body() decorator now relies on the global ValidationPipe configured in main.ts
      async sendSms(
        @Body() sendSmsDto: SendSmsDto,
      ): Promise<{ messageId: string; status: string }> {
        this.logger.log(`Received request to send SMS to ${sendSmsDto.phoneNumber}`);
        try {
          // DTO validation is handled automatically by the global ValidationPipe
          const messageId = await this.smsService.sendSms(
            sendSmsDto.phoneNumber,
            sendSmsDto.message,
          );
          return { messageId, status: 'SMS request accepted for delivery.' };
        } catch (error) {
          // Catch specific exceptions if needed, or rethrow
          if (error instanceof AwsServiceUnavailableException) {
            // Re-throw to let NestJS handle it (or use an Exception Filter)
            throw error;
          }
          // Handle other potential errors from the service layer
          this.logger.error('Unhandled error in sendSms endpoint', error);
          // Throw a generic NestJS exception for unexpected errors
          throw new Error('An unexpected error occurred while processing the SMS request.'); // Generic fallback
        }
      }
    }
  4. Enable Global Validation Pipe: For DTO validation to work automatically, enable the ValidationPipe globally in src/main.ts. This is the recommended approach.

    typescript
    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ValidationPipe, Logger } from '@nestjs/common'; // Import ValidationPipe and Logger
    import { ConfigService } from '@nestjs/config'; // Import ConfigService
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      const configService = app.get(ConfigService); // Get ConfigService instance
      const logger = new Logger('Bootstrap'); // Create a logger instance
    
      // Enable global DTO validation
      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 non-whitelisted properties are present
        transformOptions: {
          enableImplicitConversion: true, // Allow basic type conversions if needed
        },
      }));
    
      const port = configService.get<number>('PORT', 3000); // Get port from env or default to 3000
      await app.listen(port);
      logger.log(`Application is running on: ${await app.getUrl()}`);
    }
    bootstrap();

    Now, any incoming request to the sendSms endpoint will have its body automatically validated against the SendSmsDto. If validation fails, NestJS will return a 400 Bad Request response automatically.

API Endpoint Testing:

You can now test the endpoint using curl or Postman. Make sure your NestJS application is running (npm run start:dev).

  • Using curl:

    bash
    curl --location --request POST 'http://localhost:3000/sms/send' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "phoneNumber": "+12065550100",
        "message": "Hello from NestJS and AWS SNS! (Test)"
    }'

    (Replace +12065550100 with a valid E.164 test phone number)

  • Expected Success Response (202 Accepted):

    json
    {
      "messageId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "status": "SMS request accepted for delivery."
    }
  • Expected Validation Error Response (400 Bad Request): If you send an invalid phone number format:

    json
    {
      "message": [
        "Phone number must be in E.164 format (e.g., +12065550100)"
      ],
      "error": "Bad Request",
      "statusCode": 400
    }

5. Error Handling and Logging

We've already implemented basic logging and error handling, but let's refine it.

  • Logging: NestJS's built-in Logger is used in both the service and controller. It logs information about initialization, incoming requests, successful sends, and errors. In a production environment, you'd typically configure more robust logging (e.g., JSON format, sending logs to CloudWatch or another aggregation service).
  • Specific Error Handling: The SmsService catches errors from the snsClient.send() call. It logs the error stack and throws a custom AwsServiceUnavailableException. The controller catches this specific exception and re-throws it, allowing NestJS's default exception filter (or a custom one) to handle generating the 503 response.
    • Why HttpStatus.ACCEPTED (202)? SNS Publish is asynchronous. A successful API call means SNS accepted the request, not that the SMS was delivered. Returning 202 reflects this. Delivery status can be tracked via SNS Delivery Status Logging (an advanced topic).
    • Why AwsServiceUnavailableException (503)? If we fail to communicate with SNS due to network issues, credential problems caught late, or throttling on the AWS side, it indicates our service's dependency is unavailable. 503 is appropriate. Validation errors result in 400 Bad Request thanks to the global ValidationPipe.
  • Retry Mechanisms: AWS SDK v3 has built-in retry mechanisms with exponential backoff for many transient network errors or throttled requests. For critical operations, you might consider adding application-level retries using libraries like async-retry for specific error types if the default SDK behavior isn't sufficient, but often it is. For sending an SMS, if the initial Publish fails critically (e.g., invalid credentials), retrying won't help. If it's throttling, the SDK handles it.

6. Security Considerations

Securing the application and credentials is vital.

  1. Input Validation: Done via class-validator in the SendSmsDto and enforced by the global ValidationPipe. This prevents invalid data from reaching the service layer and mitigates risks like injection attacks if the message content were used insecurely elsewhere (though less likely for SMS).
  2. Secure Credential Management:
    • AWS credentials (Access Key ID, Secret Access Key) are stored in the .env file for local development.
    • Crucially, ensure .env is listed in your .gitignore file to prevent accidentally committing secrets to version control.
    • In production environments, avoid storing credentials directly in files. Use more secure methods like:
      • IAM Roles for EC2/ECS/Lambda: If deploying on AWS compute services, assign an IAM Role with the required SNS permissions to the instance/task/function. The SDK will automatically retrieve temporary credentials. This is the most secure method.
      • AWS Secrets Manager or Parameter Store: Store credentials securely and fetch them at runtime.
      • Environment Variables in Deployment Platform: Platforms like Heroku, Vercel, or CI/CD systems provide secure ways to inject environment variables.
  3. Rate Limiting: Protect your API from abuse and control costs by implementing rate limiting.
    • Install the throttler module:
      bash
      npm install --save @nestjs/throttler
      # or
      yarn add @nestjs/throttler
    • Configure it in src/app.module.ts:
      typescript
      // src/app.module.ts
      import { Module } from '@nestjs/common';
      import { AppController } from './app.controller';
      import { AppService } from './app.service';
      import { ConfigModule } from '@nestjs/config';
      import { SmsModule } from './sms/sms.module';
      import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; // Import throttler
      import { APP_GUARD } from '@nestjs/core'; // Import APP_GUARD
      
      @Module({
        imports: [
          ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
          ThrottlerModule.forRoot([{ // Configure throttler
            ttl: 60000, // Time-to-live in milliseconds (e.g., 60 seconds)
            limit: 10, // Max requests per ttl per IP
          }]),
          SmsModule,
        ],
        controllers: [AppController],
        providers: [
          AppService,
          { // Apply ThrottlerGuard globally
            provide: APP_GUARD,
            useClass: ThrottlerGuard,
          },
        ],
      })
      export class AppModule {}
      This configuration limits each IP address to 10 requests per 60 seconds across the entire application. You can apply more granular limits per-route if needed.
  4. IAM Least Privilege: As mentioned in Section 2, ensure the IAM user or role used has only the permissions required (sns:Publish, potentially sns:SetSMSAttributes, sns:CheckIfPhoneNumberIsOptedOut if used) and not broad permissions like AmazonSNSFullAccess, especially in production.

7. Testing

Testing ensures the different parts of the application work correctly.

  1. Unit Testing SmsService: Mock the SNSClient to avoid making actual AWS calls.

    typescript
    // src/sms/sms.service.spec.ts (Example - requires setting up mocks)
    import { Test, TestingModule } from '@nestjs/testing';
    import { SmsService } from './sms.service';
    import { ConfigService } from '@nestjs/config';
    import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
    import { AwsServiceUnavailableException } from './exceptions/aws-service-unavailable.exception';
    import { mockClient } from 'aws-sdk-client-mock'; // Use aws-sdk-client-mock
    import 'aws-sdk-client-mock-jest'; // Optional: for jest matchers
    
    // Mock ConfigService values
    const mockConfigService = {
      get: jest.fn((key: string, defaultValue?: any) => {
        if (key === 'AWS_REGION') return 'us-east-1';
        if (key === 'AWS_ACCESS_KEY_ID') return 'test-key-id';
        if (key === 'AWS_SECRET_ACCESS_KEY') return 'test-secret-key';
        if (key === 'AWS_SNS_DEFAULT_SMS_TYPE') return 'Transactional';
        return defaultValue;
      }),
    };
    
    // Mock SNSClient using aws-sdk-client-mock
    const snsMock = mockClient(SNSClient);
    
    describe('SmsService', () => {
      let service: SmsService;
    
      beforeEach(async () => {
        // Reset mock before each test
        snsMock.reset();
    
        const module: TestingModule = await Test.createTestingModule({
          providers: [
            SmsService,
            { provide: ConfigService, useValue: mockConfigService },
          ],
        }).compile();
    
        service = module.get<SmsService>(SmsService);
      });
    
      it('should be defined', () => {
        expect(service).toBeDefined();
      });
    
      it('should send an SMS successfully', async () => {
        const phoneNumber = '+15551234567';
        const message = 'Test message';
        const expectedMessageId = 'mock-message-id-123';
    
        // Mock the send command for SNSClient
        snsMock.on(PublishCommand).resolves({ MessageId: expectedMessageId });
    
        const messageId = await service.sendSms(phoneNumber, message);
    
        expect(messageId).toEqual(expectedMessageId);
        // Check if send was called with correct parameters
        expect(snsMock).toHaveReceivedCommandWith(PublishCommand, {
          PhoneNumber: phoneNumber,
          Message: message,
          MessageAttributes: {
            'AWS.SNS.SMS.SMSType': { DataType: 'String', StringValue: 'Transactional' },
          },
        });
      });
    
      it('should throw AwsServiceUnavailableException on SNS error', async () => {
        const phoneNumber = '+15551234567';
        const message = 'Test message';
        const errorMessage = 'SNS simulated error';
    
        // Mock the send command to reject
        snsMock.on(PublishCommand).rejects(new Error(errorMessage));
    
        // Expect the specific exception and message
        await expect(service.sendSms(phoneNumber, message))
          .rejects.toThrow(new AwsServiceUnavailableException(`Failed to send SMS via AWS SNS: ${errorMessage}`));
      });
    
      // Test for invalid E.164 format removed as validation moved to DTO/Controller layer
      // If service validation were kept, a test like this would be needed:
      // it('should throw BadRequestException for invalid E.164 format', async () => { ... });
    });

    (Note: You'll need to install aws-sdk-client-mock and potentially @types/jest: npm install --save-dev aws-sdk-client-mock jest @types/jest ts-jest or yarn add --dev aws-sdk-client-mock jest @types/jest ts-jest and configure Jest if not already set up by Nest CLI)

  2. Unit Testing SmsController: Mock the SmsService and any global guards applied.

    typescript
    // src/sms/sms.controller.spec.ts (Example)
    import { Test, TestingModule } from '@nestjs/testing';
    import { SmsController } from './sms.controller';
    import { SmsService } from './sms.service';
    import { SendSmsDto } from './dto/send-sms.dto';
    import { AwsServiceUnavailableException } from './exceptions/aws-service-unavailable.exception';
    import { ThrottlerGuard } from '@nestjs/throttler'; // Import guard if applied globally
    // No need to import ConfigModule or ThrottlerModule itself for basic guard mocking
    
    // Mock SmsService
    const mockSmsService = {
      sendSms: jest.fn(),
    };
    
    describe('SmsController', () => {
      let controller: SmsController;
    
      beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
          controllers: [SmsController],
          providers: [
            { provide: SmsService, useValue: mockSmsService },
            // If ThrottlerGuard is applied globally via APP_GUARD,
            // you might need to mock it or provide a mock implementation
            // to prevent interference during unit tests.
            // Example: Mocking the guard to always allow requests:
            {
              provide: ThrottlerGuard,
              useValue: { canActivate: jest.fn(() => true) },
            },
          ],
        })
        // If using global pipes/guards, sometimes overriding them in tests is needed:
        // .overrideGuard(ThrottlerGuard)
        // .useValue({ canActivate: jest.fn(() => true) })
        .compile();
    
        controller = module.get<SmsController>(SmsController);
        // Reset mocks before each test if needed
        jest.clearAllMocks();
      });
    
      it('should be defined', () => {
        expect(controller).toBeDefined();
      });
    
      it('should call SmsService.sendSms and return messageId on success', async () => {
        const sendSmsDto: SendSmsDto = {
          phoneNumber: '+15559876543',
          message: 'Controller test',
        };
        const expectedMessageId = 'controller-mock-id-456';
        const expectedStatus = 'SMS request accepted for delivery.';
    
        // Setup mock implementation for sendSms
        mockSmsService.sendSms.mockResolvedValue(expectedMessageId);
    
        const result = await controller.sendSms(sendSmsDto);
    
        expect(mockSmsService.sendSms).toHaveBeenCalledWith(
          sendSmsDto.phoneNumber,
          sendSmsDto.message,
        );
        expect(result).toEqual({ messageId: expectedMessageId, status: expectedStatus });
      });
    
      it('should re-throw AwsServiceUnavailableException from service', async () => {
        const sendSmsDto: SendSmsDto = {
          phoneNumber: '+15559876543',
          message: 'Controller error test',
        };
        const exception = new AwsServiceUnavailableException('Service layer error');
    
        // Setup mock to throw the specific exception
        mockSmsService.sendSms.mockRejectedValue(exception);
    
        await expect(controller.sendSms(sendSmsDto)).rejects.toThrow(exception);
        expect(mockSmsService.sendSms).toHaveBeenCalledWith(
          sendSmsDto.phoneNumber,
          sendSmsDto.message,
        );
      });
    
      it('should throw a generic error for unexpected service errors', async () => {
        const sendSmsDto: SendSmsDto = {
          phoneNumber: '+15559876543',
          message: 'Controller generic error test',
        };
        const genericError = new Error('Some unexpected service failure');
    
        // Setup mock to throw a generic error
        mockSmsService.sendSms.mockRejectedValue(genericError);
    
        await expect(controller.sendSms(sendSmsDto)).rejects.toThrow(
          'An unexpected error occurred while processing the SMS request.',
        );
        expect(mockSmsService.sendSms).toHaveBeenCalledWith(
          sendSmsDto.phoneNumber,
          sendSmsDto.message,
        );
      });
    
      // Note: DTO validation testing is typically handled by e2e tests or implicitly
      // trusted due to the global ValidationPipe. Unit tests focus on the controller's
      // interaction with the service assuming valid input.
    });
  3. End-to-End (E2E) Testing: Use NestJS's built-in E2E testing capabilities (supertest) to test the entire flow from HTTP request to response, including validation, controller logic, and potentially mocking the AWS SDK at a higher level or using tools like LocalStack for local AWS emulation. E2E tests provide the highest confidence but are slower and more complex to set up.

Frequently Asked Questions

How to send SMS messages with NestJS?

Integrate AWS SNS into your NestJS application. This involves setting up your project with the necessary AWS SDK, configuring your environment variables, creating an AWS IAM user with SNS permissions, implementing an SMS service in NestJS to handle the sending logic, and exposing this service via a controller with a REST API endpoint. This setup will enable your NestJS backend to send SMS messages programmatically.

What is AWS SNS used for in NestJS SMS?

AWS SNS (Simple Notification Service) is used as the messaging service to deliver SMS messages directly to phone numbers. It is chosen for its direct SMS sending capability, scalability, reliability, and integration with the AWS ecosystem. SNS handles the complexities of telephony infrastructure, allowing developers to focus on application logic.

Why use NestJS for sending SMS messages?

NestJS provides a structured and efficient way to build server-side applications. Its modular architecture, dependency injection, and TypeScript support make it easier to manage dependencies, test code, and maintain the application, especially when integrating with external services like AWS SNS.

How to set up AWS credentials for sending SMS?

Create an IAM user in your AWS account and grant it permissions to use SNS, at least the "sns:Publish" action. Generate an access key ID and secret access key for this user. Store these credentials securely, preferably not directly in files, and load them into your NestJS application using environment variables or a more secure method like AWS Secrets Manager for production.

What AWS region should I use for sending SMS with SNS?

Not all AWS regions support SMS sending. Choose a region like us-east-1 (N. Virginia), us-west-2 (Oregon), or others listed in the AWS documentation for SNS supported regions. Ensure the region you select in your AWS configuration matches the region your SNS service is configured for.

How to handle errors when sending SMS messages?

Implement error handling within your NestJS SMS service using try-catch blocks to capture errors during SNS interactions. Throw a custom exception such as `AwsServiceUnavailableException` to provide more specific HTTP responses, for example a 503 status code for service unavailability. Log the errors for debugging and monitoring.

How to validate phone numbers in NestJS SMS app?

Use a data transfer object (DTO) and class-validator. Create a DTO (e.g., `SendSmsDto`) for the API request and use decorators like `@IsString`, `@IsNotEmpty`, and `@Matches` with a regular expression for E.164 phone number format validation in the DTO class. Enable a global validation pipe in your NestJS application (`main.ts`) to automatically validate incoming requests against the DTO. This will reject invalid phone number formats with 400 Bad Request errors.

What is the purpose of the AWS.SNS.SMS.SMSType message attribute?

The `AWS.SNS.SMS.SMSType` attribute determines how AWS SNS handles SMS delivery. Setting it to 'Transactional' makes messages suitable for critical alerts and OTPs (One-Time Passwords) because they have higher priority and are more likely to bypass DND. 'Promotional' is more cost-effective for marketing messages.

How to manage AWS credentials securely in production?

Avoid storing AWS credentials directly in files. For production, use IAM roles for EC2, ECS, or Lambda. This automatically handles credentials. You can also utilize AWS Secrets Manager or Parameter Store to store credentials and retrieve them during runtime within your NestJS application.

How to protect my NestJS SMS API from abuse?

Implement rate limiting using the `@nestjs/throttler` module. Configure it globally or per route to restrict the number of requests per IP within a time window (e.g., 10 requests per 60 seconds). This helps prevent excessive usage, denial-of-service attacks, and keeps costs under control.

How to unit test the NestJS SMS service?

Mock the `SNSClient` from the AWS SDK to avoid actual calls to AWS during testing. Utilize a mocking library like `aws-sdk-client-mock` to simulate successful and failed responses from SNS. This enables isolated testing of the SMS service logic. Mock the `ConfigService` to provide test values for AWS credentials and region without accessing environment variables.

How to implement SMS rate limiting in NestJS?

Install `@nestjs/throttler`. Add `ThrottlerModule` to your imports and configure limits (e.g., `ttl: 60000`, `limit: 10` for 10 requests every 60 seconds). Include `ThrottlerGuard` as a global guard to enforce the rate limits. You can apply this at the global level or just for specific controllers.

When to use Transactional vs Promotional SMS type?

Use 'Transactional' for critical messages like one-time passwords (OTPs) and alerts where high deliverability is essential. 'Promotional' is better for marketing messages where cost is a primary concern. Remember transactional messages might bypass DND registries but are more expensive.