code examples
code examples
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:
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.
-
Create a new NestJS project: Open your terminal and run the Nest CLI command:
bashnpx @nestjs/cli new nestjs-sns-sms cd nestjs-sns-smsChoose your preferred package manager (npm or yarn) when prompted.
-
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).
-
Configure Environment Variables: NestJS encourages using a
.envfile for environment-specific configurations, especially sensitive data like AWS credentials.-
Create a
.envfile 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=TransactionalImportant: Replace
YOUR_AWS_ACCESS_KEY_IDandYOUR_AWS_SECRET_ACCESS_KEYwith the actual credentials you'll generate in the next step. Add this.envfile to your.gitignoreto 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 theConfigServiceavailable throughout the application without needing to importConfigModuleeverywhere.
-
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.
-
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, addsns:SetSMSAttributesif setting type per message or other attributes, andsns:CheckIfPhoneNumberIsOptedOutif checking opt-out status. You can restrict theResourcefrom"*"to specific topic ARNs if not sending directly to phones.Give the policy a name (e.g.,json{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "sns:Publish", "sns:SetSMSAttributes", "sns:CheckIfPhoneNumberIsOptedOut" ], "Resource": "*" } ] }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 IDandSecret access key. The secret key is only shown once. Store them securely. - Paste these keys into your
.envfile for theAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYvariables.
-
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), andap-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-1is often a safe default choice if you don't have specific region requirements. -
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
.envfile). - 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
TransactionalorPromotional. - Set a Monthly SMS spend limit (USD) to prevent unexpected costs (e.g.,
1.00for 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_TYPEvariable from.envif 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.
-
Generate the SMS Module and Service: Use the Nest CLI to generate a module and service for SMS functionality.
bashnpx @nestjs/cli generate module sms npx @nestjs/cli generate service sms --no-spec # --no-spec skips test file generation for nowThis creates
src/sms/sms.module.tsandsrc/sms/sms.service.ts. TheSmsModulewas already imported intoAppModuleearlier. -
Implement the
SmsService: Opensrc/sms/sms.service.tsand 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> { ... } } -
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.
-
Ensure
ConfigServiceis Available: Make sureConfigModuleis imported correctly insrc/app.module.tsand configured asisGlobal: true. TheSmsServiceuses@nestjs/config'sConfigServicevia dependency injection to securely retrieve the AWS credentials and region from the environment variables loaded from.env.
Explanation:
- The
SNSClientis initialized in the constructor using credentials and region fetched fromConfigService. - The
sendSmsmethod constructs thePublishCommandInputrequired 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 theAWS.SNS.SMS.SMSTypeattribute here based on our configuration. This ensures the message is treated asTransactionalorPromotionalas intended.
- The
snsClient.send()method sends the command to AWS SNS. - Error handling is included using a
try...catchblock, logging errors and throwing a customAwsServiceUnavailableException.
4. Building the API Layer
Let's expose the SMS sending functionality through a REST API endpoint.
-
Generate the SMS Controller:
bashnpx @nestjs/cli generate controller sms --no-specThis creates
src/sms/sms.controller.ts. -
Create a Data Transfer Object (DTO) for Validation: Create a file
src/sms/dto/send-sms.dto.tsto 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 thephoneNumberagainst the E.164 regex pattern.@MaxLength(): Basic check for message length (SNS has limits, typically 140 bytes for GSM-7, less for UCS-2).
-
Implement the
SmsController: Opensrc/sms/sms.controller.tsand 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 } } } -
Enable Global Validation Pipe: For DTO validation to work automatically, enable the
ValidationPipeglobally insrc/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
sendSmsendpoint will have its body automatically validated against theSendSmsDto. 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:bashcurl --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
+12065550100with 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
Loggeris 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
SmsServicecatches errors from thesnsClient.send()call. It logs the error stack and throws a customAwsServiceUnavailableException. 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)? SNSPublishis 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 globalValidationPipe.
- Why
- 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-retryfor specific error types if the default SDK behavior isn't sufficient, but often it is. For sending an SMS, if the initialPublishfails 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.
- Input Validation: Done via
class-validatorin theSendSmsDtoand enforced by the globalValidationPipe. 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). - Secure Credential Management:
- AWS credentials (
Access Key ID,Secret Access Key) are stored in the.envfile for local development. - Crucially, ensure
.envis listed in your.gitignorefile 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.
- AWS credentials (
- 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: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.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 {}
- Install the throttler module:
- IAM Least Privilege: As mentioned in Section 2, ensure the IAM user or role used has only the permissions required (
sns:Publish, potentiallysns:SetSMSAttributes,sns:CheckIfPhoneNumberIsOptedOutif used) and not broad permissions likeAmazonSNSFullAccess, especially in production.
7. Testing
Testing ensures the different parts of the application work correctly.
-
Unit Testing
SmsService: Mock theSNSClientto 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-mockand potentially@types/jest:npm install --save-dev aws-sdk-client-mock jest @types/jest ts-jestoryarn add --dev aws-sdk-client-mock jest @types/jest ts-jestand configure Jest if not already set up by Nest CLI) -
Unit Testing
SmsController: Mock theSmsServiceand 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. }); -
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.