code examples

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

How to Send SMS with NestJS and Vonage API: Step-by-Step Tutorial (2025)

Complete guide to sending SMS messages with NestJS and Vonage Messages API. Includes Vonage SDK installation, authentication setup, REST API integration, input validation, error handling, and production deployment for Node.js applications.

Send SMS with NestJS and Vonage Messages API: Complete Tutorial

Quick Answer: Send SMS with NestJS and Vonage in 5 Steps

To send SMS with NestJS and Vonage: (1) Install @vonage/server-sdk via npm, (2) Configure Vonage Application ID and private key using NestJS ConfigModule, (3) Create an SMS service that initializes the Vonage client, (4) Build a controller with a validated POST endpoint (/sms/send), (5) Call vonage.messages.send() with proper error handling. This complete tutorial covers setup, authentication, validation, testing, and production deployment.


This comprehensive guide walks you through building a production-ready NestJS application for sending SMS messages using the Vonage Messages API. You'll learn project setup, Vonage authentication, service implementation, API endpoint creation, input validation, error handling, security best practices, testing strategies, and deployment considerations.

By the end of this tutorial, you will have a fully functional NestJS SMS service with a REST API endpoint that sends text messages reliably through Vonage. This solves common business needs like sending notifications, alerts, verification codes, and OTPs (One-Time Passwords) programmatically.

Technologies Used:

  • Node.js: The JavaScript runtime environment for server-side applications.
  • NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Chosen for its modular architecture, built-in dependency injection, and excellent support for validation and configuration management. Current stable version: v11.1.6 (released January 2025) includes improved ConsoleLogger, microservices enhancements, and performance improvements.
  • Vonage Messages API: A unified API for sending messages across multiple channels including SMS, MMS, WhatsApp, and Viber. Chosen for its robust features, global reach, competitive pricing, and developer-friendly SDK.
  • Vonage Node.js Server SDK: The official SDK for interacting with Vonage APIs from Node.js applications. Current version: v3.24.1 (December 2024) provides comprehensive API support for SMS, Voice, Text-to-Speech, Numbers, Verify (2FA verification), and more.
  • dotenv / @nestjs/config: For managing environment variables and API credentials securely.

Prerequisites:

  • Node.js (v22 LTS recommended – Active LTS until October 2025, then Maintenance LTS until April 2027) and npm or yarn package manager installed.
  • A Vonage API account (Sign up here – free trial credits available).
  • Access to a terminal or command prompt.
  • Basic understanding of TypeScript, Node.js, and REST APIs.
  • Optional: Postman or curl for testing API endpoints.

Final Outcome:

A production-ready NestJS application with a POST endpoint (/sms/send) that accepts a recipient phone number and message text, validates the input, and sends the SMS via the Vonage Messages API with comprehensive error handling.

(Note: This guide focuses on sending SMS messages. To receive inbound SMS or delivery receipts, you need to configure webhooks, which is covered in the Vonage webhooks documentation but is beyond the scope of this tutorial.)


1. Setting up the NestJS Project

First, create a new NestJS project and install the necessary dependencies for SMS functionality.

  1. Install NestJS CLI (Command Line Interface): If you don't have it installed globally, run:

    bash
    npm install -g @nestjs/cli
  2. Create NestJS Project: Navigate to your desired parent directory in the terminal and run:

    bash
    nest new vonage-sms-app

    Choose your preferred package manager (npm or yarn) when prompted. This command scaffolds a new NestJS project with a standard structure including TypeScript configuration, testing setup, and essential dependencies.

  3. Navigate to Project Directory:

    bash
    cd vonage-sms-app
  4. Install Dependencies: Install the Vonage Server SDK and NestJS's configuration module.

    bash
    npm install @vonage/server-sdk @nestjs/config dotenv
    # or using yarn:
    # yarn add @vonage/server-sdk @nestjs/config dotenv
    • @vonage/server-sdk: The official Vonage library for Node.js.
    • @nestjs/config: For managing environment variables within NestJS applications.
    • dotenv: To load environment variables from a .env file during development.

Project Structure Overview:

Your initial project structure will look something like this:

text
vonage-sms-app/
├── node_modules/
├── src/
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test/
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── nest-cli.json
├── package.json
├── README.md
├── tsconfig.build.json
└── tsconfig.json

You will primarily work within the src directory, creating new modules, controllers, and services for SMS functionality.


2. Configuring Vonage Messages API Credentials

To interact with the Vonage API, you need proper authentication credentials and a Vonage phone number. The Vonage Messages API requires an Application ID and a private key for secure authentication.

  1. Log in to Vonage Dashboard: Access your Vonage API Dashboard.

  2. Find API Key and Secret: Your API Key and Secret are displayed at the top of the dashboard home page. Note these down as you may need them for certain operations or when using the Vonage CLI tool.

  3. Set Messages API as Default (Crucial):

    • Navigate to API Settings in the dashboard.
    • Under "SMS Settings," ensure "Default SMS Setting" is set to Messages API.
    • Click "Save changes". This ensures your account uses the correct API backend for sending when using Application ID/Private Key authentication.
  4. Create a Vonage Application:

    • Navigate to "Applications" → "Create a new application".
    • Enter an Application Name (e.g., "NestJS SMS App").
    • Click "Generate public and private key". This will automatically download a private.key file. Save this file securely – you'll place it in your project root later. The public key is stored by Vonage automatically.
    • Enable the Messages capability by checking the appropriate box.
    • For the Status URL and Inbound URL under Messages, you can initially enter dummy HTTPS URLs (e.g., https://example.com/status, https://example.com/inbound). These are required fields for receiving messages or delivery receipts, but not strictly necessary for sending only. However, Vonage requires valid HTTPS URLs here.
    • Click "Generate new application".
    • Note down the Application ID generated for this application – you'll need it for authentication.
  5. Link a Vonage Number:

    • You need a Vonage virtual phone number to send SMS from. If you don't have one, go to "Numbers" → "Buy numbers" and purchase an SMS-capable number.
    • Go to "Numbers" → "Your numbers".
    • Find the number you want to use and click the gear icon (Manage) or the number itself.
    • Under "Forwarding", select "Forward to App" and choose the application you just created ("NestJS SMS App").
    • Click "Save".
    • Note down the Vonage Number you linked in E.164 format (international phone number format, e.g., 14155550100).
  6. Configure Environment Variables:

    • Place the private.key file you downloaded into the root directory of your vonage-sms-app project.
    • Create a file named .env in the project root directory.
    • Add the following variables, replacing the placeholder values with your actual credentials and file path:
    dotenv
    # .env
    
    # Vonage Credentials (Messages API)
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
    VONAGE_PRIVATE_KEY_PATH=./private.key
    
    # Vonage Number (E.164 format)
    VONAGE_NUMBER=YOUR_VONAGE_NUMBER_E164
    
    # Optional: API Key/Secret (useful for Vonage CLI or other APIs)
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Application Port
    PORT=3000
    • VONAGE_APPLICATION_ID: The unique ID of the Vonage application you created. Found on the Application details page in the dashboard.
    • VONAGE_PRIVATE_KEY_PATH: The relative path from your project root to the private.key file you downloaded.
    • VONAGE_NUMBER: The Vonage virtual phone number you linked to the application, in E.164 format (e.g., 14155550100). This will be the "from" number for outgoing SMS messages.
    • VONAGE_API_KEY / VONAGE_API_SECRET: Your main account API key/secret. Not directly used for sending via Messages API in this code, but good to store for other potential uses (like CLI operations or other Vonage services).
    • PORT: The port your NestJS application will run on (default is 3000).
  7. Update .gitignore: Ensure your .env file and private.key are not committed to version control. Add these lines to your .gitignore file if they aren't already present:

    text
    # .gitignore
    
    .env
    private.key
    node_modules
    dist
  8. Load Configuration in NestJS: Modify src/app.module.ts to load the .env file using ConfigModule.

    typescript
    // src/app.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config'; // Import ConfigModule
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    // You will create SmsModule later
    // import { SmsModule } from './sms/sms.module';
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true, // Make ConfigModule available globally
          envFilePath: '.env', // Specify the path to the .env file
        }),
        // SmsModule, // Uncomment this later
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
    • ConfigModule.forRoot({...}): Initializes the configuration module.
    • isGlobal: true: Makes the ConfigService available throughout your application without needing to import ConfigModule in every feature module.
    • envFilePath: '.env': Tells the module where to find the environment variables file.

3. Implementing the SMS Service with Dependency Injection

Now, create a dedicated module and service in NestJS to handle the logic for sending SMS messages using proper dependency injection patterns.

  1. Generate SMS Module and Service: Use the NestJS CLI to generate the necessary files:

    bash
    nest generate module sms
    nest generate service sms

    This creates a src/sms directory containing sms.module.ts and sms.service.ts (and spec file for testing).

  2. Implement SmsService: Open src/sms/sms.service.ts and implement the logic to initialize the Vonage SDK and send messages.

    typescript
    // src/sms/sms.service.ts
    import { Injectable, Logger } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import { Vonage } from '@vonage/server-sdk';
    import { MessageSendRequest } from '@vonage/messages'; // Import specific type
    import * as fs from 'fs'; // Import Node.js fs module
    
    @Injectable()
    export class SmsService {
      private readonly logger = new Logger(SmsService.name);
      private vonage: Vonage;
      private vonageNumber: string;
    
      constructor(private configService: ConfigService) {
        const applicationId = this.configService.get<string>('VONAGE_APPLICATION_ID');
        const privateKeyPath = this.configService.get<string>('VONAGE_PRIVATE_KEY_PATH');
        this.vonageNumber = this.configService.get<string>('VONAGE_NUMBER');
    
        // Ensure private key path is valid and file exists
        if (!privateKeyPath || !fs.existsSync(privateKeyPath)) {
            this.logger.error(`Private key file not found at path: ${privateKeyPath}`);
            throw new Error('Vonage private key file not found.');
        }
         // Read the private key content
        const privateKey = fs.readFileSync(privateKeyPath);
    
        if (!applicationId || !privateKey || !this.vonageNumber) {
          this.logger.error('Vonage credentials (App ID, Private Key Path, or Number) are missing in environment variables.');
          throw new Error('Missing Vonage configuration.');
        }
    
        this.vonage = new Vonage({
          applicationId: applicationId,
          privateKey: privateKey, // Pass the key content directly
        });
    
        this.logger.log('Vonage client initialized successfully.');
      }
    
      async sendSms(to: string, text: string): Promise<string | null> {
        this.logger.log(`Attempting to send SMS to ${to}`);
    
        // Input validation for 'to' number format is handled by the DTO/ValidationPipe at the controller level.
        // E.164 format (e.g., +14155550100) is strongly recommended for Vonage.
    
        const payload: MessageSendRequest = {
          message_type: 'text',
          to: to,
          from: this.vonageNumber, // Use the configured Vonage number
          channel: 'sms',
          text: text,
        };
    
        try {
          const response = await this.vonage.messages.send(payload);
          this.logger.log(`SMS sent successfully to ${to}. Message UUID: ${response.message_uuid}`);
          return response.message_uuid;
        } catch (error) {
          this.logger.error(`Failed to send SMS to ${to}: ${error.message}`, error.stack);
          // You might want to inspect 'error' further. Vonage errors often have specific properties.
          // e.g., error.response?.data or similar depending on the error structure
          throw new Error(`Failed to send SMS via Vonage: ${error.message}`);
        }
      }
    }
    • Constructor: Injects ConfigService to retrieve environment variables. Initializes the Vonage SDK instance using the Application ID and the content of the private key file (read using fs.readFileSync). It includes checks to ensure credentials and the key file exist.
    • sendSms Method:
      • Takes the recipient number (to) and message text as arguments.
      • Relies on controller-level validation for the to number format.
      • Constructs the payload object according to the Vonage Messages API requirements for SMS (message_type: 'text', channel: 'sms', to, from, text).
      • Uses this.vonage.messages.send(payload) to send the SMS. Note the use of messages, not message.
      • Uses async/await for cleaner asynchronous code.
      • Includes logging for success and failure using NestJS's Logger.
      • Returns the message_uuid on success or throws an error on failure.
  3. Update SmsModule: Ensure SmsService is listed as a provider and exported so it can be used in other modules (like the controller you'll create next).

    typescript
    // src/sms/sms.module.ts
    import { Module } from '@nestjs/common';
    import { SmsService } from './sms.service';
    // You will create SmsController later
    // import { SmsController } from './sms.controller';
    
    @Module({
      // controllers: [SmsController], // Uncomment this later
      providers: [SmsService],
      exports: [SmsService], // Export SmsService
    })
    export class SmsModule {}
  4. Import SmsModule in AppModule: Now uncomment the SmsModule import in src/app.module.ts.

    typescript
    // src/app.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { SmsModule } from './sms/sms.module'; // Import SmsModule
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true,
          envFilePath: '.env',
        }),
        SmsModule, // Add SmsModule here
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}

4. Building the SMS API Endpoint with Validation

Create a controller with an endpoint that accepts POST requests to trigger the SMS sending functionality with proper input validation.

  1. Generate SMS Controller:

    bash
    nest generate controller sms

    This creates src/sms/sms.controller.ts (and spec file for testing).

  2. Create Request DTO (Data Transfer Object): Create a file src/sms/dto/send-sms.dto.ts for validating the incoming request body. Install validation dependencies if you haven't already:

    bash
    npm install class-validator class-transformer
    # or using yarn:
    # yarn add class-validator class-transformer

    Note: This uses class-validator v0.14.2 (May 2024) and class-transformer (actively maintained), which are the most commonly used validation packages in NestJS due to their decorator-based approach that integrates seamlessly with NestJS's ValidationPipe.

    Now, define the DTO:

    typescript
    // src/sms/dto/send-sms.dto.ts
    import { IsNotEmpty, IsString, IsPhoneNumber, Length } from 'class-validator';
    
    export class SendSmsDto {
      @IsNotEmpty()
      @IsPhoneNumber(null, { message: 'Recipient phone number must be a valid E.164 format phone number (e.g., +14155550100)' }) // Use null for region code to allow international numbers
      readonly to: string;
    
      @IsNotEmpty()
      @IsString()
      @Length(1, 1600) // Vonage SMS length limit: 160 GSM-7 chars (single), 153 chars/segment (concatenated with UDH), or 70 unicode chars/segment
      readonly text: string;
    }
    • Use decorators from class-validator to define validation rules.
    • @IsNotEmpty(): Ensures the field is not empty.
    • @IsPhoneNumber(null): Validates if the string is a phone number (accepts various formats, but E.164 is recommended for Vonage). Setting region to null allows international numbers.
    • @IsString(): Ensures the field is a string.
    • @Length(1, 1600): Ensures the message text has a reasonable length. Vonage SMS limits:
      • Single SMS: 160 characters (GSM-7 encoding) or 70 characters (unicode/UCS-2 encoding)
      • Concatenated SMS: Messages over 160 characters split into 153-character segments (7 bytes reserved for UDH), or 67-character segments for unicode
      • Maximum recommended: ~1600 characters (approximately 10 concatenated segments)
      • Reference: Vonage SMS API Documentation
  3. Implement SmsController: Open src/sms/sms.controller.ts and define the POST endpoint.

    typescript
    // src/sms/sms.controller.ts
    import { Controller, Post, Body, ValidationPipe, UsePipes, Logger, HttpException, HttpStatus } from '@nestjs/common';
    import { SmsService } from './sms.service';
    import { SendSmsDto } from './dto/send-sms.dto';
    
    @Controller('sms') // Route prefix: /sms
    export class SmsController {
      private readonly logger = new Logger(SmsController.name);
    
      constructor(private readonly smsService: SmsService) {}
    
      @Post('send') // Full route: POST /sms/send
      @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) // Enable validation
      async sendSms(@Body() sendSmsDto: SendSmsDto) {
        this.logger.log(`Received request to send SMS to: ${sendSmsDto.to}`);
        try {
          const messageUuid = await this.smsService.sendSms(sendSmsDto.to, sendSmsDto.text);
          this.logger.log(`SMS queued successfully. Message UUID: ${messageUuid}`);
          // NestJS automatically sends a 201 Created status for POST requests by default
          return {
            success: true,
            message: 'SMS sent successfully.',
            messageId: messageUuid,
          };
        } catch (error) {
          this.logger.error(`Error sending SMS via controller: ${error.message}`, error.stack);
          // Throw a standard NestJS HTTP exception
          throw new HttpException(
             {
                success: false,
                message: `Failed to send SMS: ${error.message || 'Unknown error'}`,
             },
             HttpStatus.INTERNAL_SERVER_ERROR, // Or potentially BAD_REQUEST depending on the error type
          );
        }
      }
    }
    • @Controller('sms'): Defines the base route for this controller.
    • Constructor: Injects the SmsService.
    • @Post('send'): Defines a handler for POST requests to /sms/send.
    • @UsePipes(new ValidationPipe(...)): Applies the validation pipe to this route.
      • transform: true: Automatically transforms the incoming JSON payload into an instance of SendSmsDto.
      • whitelist: true: Strips any properties from the request body that are not defined in the DTO.
    • @Body() sendSmsDto: SendSmsDto: Injects the validated and transformed request body into the sendSmsDto parameter.
    • Logic: Calls the smsService.sendSms method. Returns a success response with the messageId. NestJS handles the 201 status. Throws an HttpException with a 500 status code if the service throws an error.
  4. Register Controller in SmsModule: Uncomment the SmsController in src/sms/sms.module.ts.

    typescript
    // src/sms/sms.module.ts
    import { Module } from '@nestjs/common';
    import { SmsService } from './sms.service';
    import { SmsController } from './sms.controller'; // Import controller
    
    @Module({
      controllers: [SmsController], // Add controller here
      providers: [SmsService],
      exports: [SmsService],
    })
    export class SmsModule {}
  5. Enable Global Validation Pipe (Optional but Recommended): Instead of applying @UsePipes to every handler, you can enable the ValidationPipe globally in src/main.ts.

    typescript
    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { Logger, ValidationPipe } from '@nestjs/common'; // Import ValidationPipe
    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 port = configService.get<number>('PORT') || 3000; // Get port from env
    
      // Enable global validation pipe
      app.useGlobalPipes(new ValidationPipe({
        whitelist: true,        // Strip properties not in DTO
        transform: true,        // Transform payloads to DTO instances
        forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are present
        transformOptions: {
           enableImplicitConversion: true, // Allow basic type conversions
        },
      }));
    
      app.enableCors(); // Optional: Enable CORS (Cross-Origin Resource Sharing) if your frontend is on a different domain
    
      await app.listen(port);
      Logger.log(`Application is running on: http://localhost:${port}`, 'Bootstrap');
    }
    bootstrap();

    If you do this, you can remove the @UsePipes(...) decorator from the SmsController.


5. Error Handling and Logging Best Practices

You've already incorporated basic logging and error handling:

  • Logging: Logger instances in both the service and controller provide context-specific logs for requests, successes, and failures. Check your console output when running the app.
  • Validation Errors: The ValidationPipe automatically handles request validation errors, returning 400 Bad Request responses with details about the validation failures.
  • Service Errors: The SmsService catches errors from the Vonage SDK (try...catch) and logs them before re-throwing.
  • Controller Errors: The SmsController catches errors from the service and transforms them into standard HttpException responses, ensuring consistent JSON error formats for the client.

Further Enhancements:

  • Custom Exception Filters: For more granular control over error responses, especially for specific Vonage error codes (e.g., insufficient funds, invalid number), implement a custom NestJS Exception Filter. This filter can inspect the error thrown by the Vonage SDK and map it to appropriate HTTP status codes and response bodies.
  • Structured Logging: For production, consider using a more robust logging library (like Pino or Winston) integrated with NestJS to output structured JSON logs, making them easier to parse and analyze in log aggregation tools (e.g., Datadog, Splunk, ELK stack).

6. Security Considerations for Production SMS Services

While this is a simple service, security is paramount:

  1. Secrets Management:
    • NEVER commit your .env file or private.key to Git. Ensure they are in .gitignore.
    • In production environments, use proper secrets management solutions provided by your cloud provider (e.g., AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) or tools like HashiCorp Vault instead of .env files. Inject these secrets as environment variables into your deployed application.
    • Restrict file permissions for private.key on your server (chmod 400 private.key).
  2. Input Validation: Already handled by the ValidationPipe and SendSmsDto. This prevents malformed requests and potential injection issues related to input data. forbidNonWhitelisted: true in the global pipe adds an extra layer of protection against unexpected input fields.
  3. Rate Limiting: To prevent abuse of your SMS endpoint (which costs money), implement rate limiting. The @nestjs/throttler module is excellent for this.
    • Install: npm install @nestjs/throttler
    • Configure in app.module.ts:
      typescript
      // src/app.module.ts
      import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
      import { APP_GUARD } from '@nestjs/core';
      // ... other imports
      
      @Module({
        imports: [
          // ... ConfigModule, SmsModule
          ThrottlerModule.forRoot([{
            ttl: 60000, // Time-to-live (milliseconds) – 60 seconds
            limit: 10,  // Max requests per TTL per IP
          }]),
        ],
        // ... controllers, providers
        providers: [
          AppService,
          { // Apply ThrottlerGuard globally
            provide: APP_GUARD,
            useClass: ThrottlerGuard,
          },
        ],
      })
      export class AppModule {}
    • This setup limits each IP address to 10 requests per 60 seconds across all endpoints protected by the guard (which is global here). Adjust ttl and limit as needed.
  4. API Authentication/Authorization: This guide doesn't implement authentication for the /sms/send endpoint itself. In a real-world application, protect this endpoint using strategies like API Keys, JWT tokens, or OAuth, ensuring only authorized clients can trigger SMS sending. NestJS provides modules and strategies for implementing these. For more details, see the NestJS authentication documentation.
  5. HTTPS: Always run your application behind HTTPS in production to encrypt traffic. Use a reverse proxy like Nginx or Caddy, or leverage your cloud provider's load balancer services.

7. Testing the SMS Implementation (Unit and E2E Tests)

Testing ensures your service works as expected and helps prevent regressions.

  1. Unit Testing (SmsService):

    • NestJS generates a spec file (sms.service.spec.ts).
    • Use @nestjs/testing to create a testing module.
    • Mock dependencies (ConfigService, Vonage SDK, fs). Jest's mocking capabilities are ideal here.
    • Test the sendSms method:
      • Verify it calls vonage.messages.send with the correct payload.
      • Test success scenarios (mock send to resolve successfully).
      • Test failure scenarios (mock send to reject with an error).
      • Test constructor error handling (e.g., missing key file).

    Example Snippet:

    typescript
    // src/sms/sms.service.spec.ts
    import { Test, TestingModule } from '@nestjs/testing';
    import { SmsService } from './sms.service';
    import { ConfigService } from '@nestjs/config';
    import { Vonage } from '@vonage/server-sdk';
    import * as fs from 'fs';
    
    // Mock fs module
    jest.mock('fs');
    const mockReadFileSync = fs.readFileSync as jest.Mock;
    const mockExistsSync = fs.existsSync as jest.Mock;
    
    // Mock Vonage SDK
    const mockMessagesSend = jest.fn();
    jest.mock('@vonage/server-sdk', () => {
      return {
        Vonage: jest.fn().mockImplementation(() => { // Mock the constructor
          return {
            messages: { // Mock the messages property
              send: mockMessagesSend, // Use the specific mock function for send
            },
          };
        }),
      };
    });
    
    describe('SmsService', () => {
      let service: SmsService;
      let configService: ConfigService;
    
      const mockConfigValues = {
        VONAGE_APPLICATION_ID: 'test-app-id',
        VONAGE_PRIVATE_KEY_PATH: './dummy-private.key',
        VONAGE_NUMBER: '15551234567',
      };
    
      const testModuleSetup = {
        providers: [
          SmsService,
          {
            provide: ConfigService,
            useValue: {
              get: jest.fn((key: string) => mockConfigValues[key] || null),
            },
          },
        ],
      };
    
      beforeEach(async () => {
        // Reset mocks before each test
        mockExistsSync.mockReturnValue(true); // Assume key exists by default
        mockReadFileSync.mockReturnValue('mock-private-key-content');
        mockMessagesSend.mockClear();
        (Vonage as jest.Mock).mockClear(); // Clear the constructor mock calls
    
        const module: TestingModule = await Test.createTestingModule(testModuleSetup).compile();
    
        service = module.get<SmsService>(SmsService);
        configService = module.get<ConfigService>(ConfigService);
      });
    
      it('should be defined', () => {
        expect(service).toBeDefined();
      });
    
      it('should initialize Vonage client with correct credentials on instantiation', () => {
        // Check if fs functions were called during instantiation (triggered by beforeEach)
        expect(mockExistsSync).toHaveBeenCalledWith(mockConfigValues.VONAGE_PRIVATE_KEY_PATH);
        expect(mockReadFileSync).toHaveBeenCalledWith(mockConfigValues.VONAGE_PRIVATE_KEY_PATH);
    
        // Check if Vonage constructor was called correctly
        expect(Vonage).toHaveBeenCalledTimes(1);
        expect(Vonage).toHaveBeenCalledWith({
          applicationId: mockConfigValues.VONAGE_APPLICATION_ID,
          privateKey: 'mock-private-key-content',
        });
      });
    
      it('should call vonage.messages.send with correct parameters', async () => {
        const to = '15559876543';
        const text = 'Test message';
        const expectedUuid = 'mock-message-uuid';
        mockMessagesSend.mockResolvedValue({ message_uuid: expectedUuid }); // Mock the send method's successful return
    
        const result = await service.sendSms(to, text);
    
        expect(mockMessagesSend).toHaveBeenCalledTimes(1);
        expect(mockMessagesSend).toHaveBeenCalledWith({
          message_type: 'text',
          to: to,
          from: mockConfigValues.VONAGE_NUMBER, // From mock ConfigService
          channel: 'sms',
          text: text,
        });
        expect(result).toEqual(expectedUuid);
      });
    
      it('should throw an error if Vonage SDK fails', async () => {
         const errorMessage = 'Vonage API Error';
         mockMessagesSend.mockRejectedValue(new Error(errorMessage)); // Mock send method failure
    
         await expect(service.sendSms('15559876543', 'Test')).rejects.toThrow(
             `Failed to send SMS via Vonage: ${errorMessage}`,
         );
         expect(mockMessagesSend).toHaveBeenCalledTimes(1);
       });
    
       it('should throw error during instantiation if private key file does not exist', async () => {
         mockExistsSync.mockReturnValue(false); // Simulate key file not existing
    
         // We expect the compilation/instantiation process itself to throw
         await expect(Test.createTestingModule(testModuleSetup).compile())
             .rejects.toThrow('Vonage private key file not found.');
       });
    
       it('should throw error during instantiation if config variables are missing', async () => {
         // Simulate missing App ID
         const moduleWithMissingConfig = await Test.createTestingModule({
           providers: [
             SmsService,
             {
               provide: ConfigService,
               useValue: {
                  get: jest.fn((key: string) => {
                      if (key === 'VONAGE_APPLICATION_ID') return undefined; // Simulate missing ID
                      return mockConfigValues[key] || null;
                  }),
               },
             },
           ],
         }).compile();
    
         // Expect the service resolution (which triggers constructor) to fail
         expect(() => moduleWithMissingConfig.get<SmsService>(SmsService))
           .toThrow('Missing Vonage configuration.');
       });
    });
  2. E2E (End-to-End) Testing (SmsController):

    • NestJS generates an e2e spec file in the test directory.
    • Use @nestjs/testing and supertest to make HTTP requests to your running application instance (or a test instance).
    • Test the /sms/send endpoint:
      • Send valid requests and check for 201 Created responses and the expected JSON structure.
      • Send invalid requests (missing fields, invalid phone number) and check for 400 Bad Request responses with validation error details.
      • Send requests that cause the service layer to throw an error (by mocking SmsService.sendSms to reject) and check for 500 Internal Server Error responses.

    Example Snippet (Conceptual):

    typescript
    // test/app.e2e-spec.ts (Illustrative fragment)
    import * as request from 'supertest';
    import { Test } from '@nestjs/testing';
    import { AppModule } from './../src/app.module';
    import { INestApplication, ValidationPipe } from '@nestjs/common';
    import { SmsService } from './../src/sms/sms.service'; // Import to mock
    
    describe('SmsController (e2e)', () => {
      let app: INestApplication;
      // Mock implementation for the service
      const mockSmsService = {
        sendSms: jest.fn(),
      };
    
      beforeAll(async () => {
        const moduleFixture = await Test.createTestingModule({
          imports: [AppModule],
        })
        .overrideProvider(SmsService) // Override the real service
        .useValue(mockSmsService)     // with our mock implementation
        .compile();
    
        app = moduleFixture.createNestApplication();
        // Apply the same global pipes used in main.ts for consistency
        app.useGlobalPipes(new ValidationPipe({
          whitelist: true,
          transform: true,
          forbidNonWhitelisted: true,
          transformOptions: {
             enableImplicitConversion: true,
          },
        }));
        await app.init();
      });
    
      afterAll(async () => {
        await app.close();
      });
    
      beforeEach(() => {
         // Reset mocks before each e2e test case
         mockSmsService.sendSms.mockClear();
      });
    
      it('/sms/send (POST) - should send SMS successfully', async () => {
        const dto = { to: '+14155550100', text: 'E2E Test Message' };
        const mockUuid = 'e2e-message-uuid';
        mockSmsService.sendSms.mockResolvedValue(mockUuid); // Mock service success
    
        return request(app.getHttpServer())
          .post('/sms/send')
          .send(dto)
          .expect(201) // Expect HTTP 201 Created
          .expect((res) => {
            expect(res.body).toEqual({
              success: true,
              message: 'SMS sent successfully.',
              messageId: mockUuid,
            });
            expect(mockSmsService.sendSms).toHaveBeenCalledWith(dto.to, dto.text);
          });
      });
    
      it('/sms/send (POST) - should return 400 on invalid phone number', async () => {
        const dto = { to: 'invalid-number', text: 'Test' };
    
        return request(app.getHttpServer())
          .post('/sms/send')
          .send(dto)
          .expect(400) // Expect HTTP 400 Bad Request
          .expect((res) => {
             expect(res.body.message).toContain('Recipient phone number must be a valid E.164 format phone number');
             expect(mockSmsService.sendSms).not.toHaveBeenCalled();
          });
      });
    
       it('/sms/send (POST) - should return 400 on missing text field', async () => {
         const dto = { to: '+14155550100' }; // Missing 'text'
    
         return request(app.getHttpServer())
           .post('/sms/send')
           .send(dto)
           .expect(400)
           .expect((res) => {
              expect(res.body.message).toEqual(expect.arrayContaining([
                  expect.stringContaining('text should not be empty'),
                  expect.stringContaining('text must be a string'),
                  expect.stringContaining('text must be longer than or equal to 1 characters'),
              ]));
              expect(mockSmsService.sendSms).not.toHaveBeenCalled();
           });
       });
    
      it('/sms/send (POST) - should return 500 if service throws error', async () => {
        const dto = { to: '+14155550100', text: 'Error Test' };
        const errorMessage = 'Internal Service Error';
        mockSmsService.sendSms.mockRejectedValue(new Error(errorMessage)); // Mock service failure
    
        return request(app.getHttpServer())
          .post('/sms/send')
          .send(dto)
          .expect(500) // Expect HTTP 500 Internal Server Error
          .expect((res) => {
            expect(res.body).toEqual({
              success: false,
              message: `Failed to send SMS: ${errorMessage}`,
            });
            expect(mockSmsService.sendSms).toHaveBeenCalledWith(dto.to, dto.text);
          });
      });
    });

Frequently Asked Questions (FAQ)

What is the difference between Vonage SMS API and Messages API?

The Vonage SMS API is the legacy API for sending SMS messages, while the Messages API is the newer, unified API that supports multiple channels (SMS, MMS, WhatsApp, Viber, Facebook Messenger) through a single interface. The Messages API uses Application ID and private key authentication instead of API Key/Secret, provides better error handling, and is the recommended approach for new applications. This tutorial uses the Messages API for maximum flexibility and future compatibility.

How much does it cost to send SMS with Vonage?

Vonage SMS pricing varies by destination country and typically ranges from $0.0037 to $0.15 per message. New accounts receive free trial credits (usually $2 – $10) to test the service. Pricing is per-segment: messages up to 160 GSM-7 characters or 70 unicode characters count as one segment, while longer messages split into 153-character (GSM-7) or 67-character (unicode) segments with each segment billed separately. Check the Vonage Pricing page for current rates in your target countries.

Can I use Vonage with NestJS for two-factor authentication (2FA)?

Yes, you can use Vonage with NestJS for 2FA/OTP verification. However, Vonage offers a dedicated Verify API specifically designed for 2FA that handles OTP generation, delivery, retry logic, and verification automatically. For simple SMS-based 2FA, you can use this tutorial's implementation to send randomly generated codes, then store and validate them server-side. For production 2FA, consider using Vonage Verify API, which provides built-in rate limiting, fraud detection, and fallback channels (SMS → voice call).

How do I receive SMS messages and delivery receipts in NestJS?

To receive inbound SMS messages and delivery receipts, you need to create webhook endpoints in your NestJS application. Create a new controller with POST endpoints for inbound messages (/webhooks/inbound-sms) and delivery receipts (/webhooks/delivery-receipt), configure these URLs in your Vonage Application settings (they must be publicly accessible HTTPS URLs), and use DTOs with class-validator to validate the webhook payloads. For local development, use ngrok to expose your localhost to the internet. The webhook payloads contain message status, timestamps, error codes, and sender information.

What is E.164 phone number format and why does Vonage require it?

E.164 is the international standard for phone number formatting: +[country code][subscriber number] with no spaces, hyphens, or parentheses (e.g., +14155550100 for a US number). Vonage requires E.164 format because it eliminates ambiguity about country codes and ensures reliable message delivery worldwide. The @IsPhoneNumber(null) validator from class-validator accepts E.164 format. Always store and transmit phone numbers in E.164 format, but you can display them with formatting for user interfaces.

How do I handle SMS sending errors in NestJS with Vonage?

The Vonage SDK throws errors with specific properties that help identify issues. Common errors include: Authentication failures (invalid Application ID or private key, HTTP 401), Insufficient credit (account balance too low, HTTP 402), Invalid phone numbers (malformed recipient, HTTP 400), Rate limiting (too many requests, HTTP 429), and Network timeouts (connectivity issues). Wrap vonage.messages.send() calls in try...catch blocks, log errors with context using NestJS Logger, inspect error.response?.data for Vonage-specific error details, throw appropriate HttpException instances with user-friendly messages, and implement retry logic for transient errors using libraries like async-retry.

Can I send bulk SMS messages with this NestJS implementation?

This tutorial's implementation sends single SMS messages per API call. For bulk messaging (hundreds or thousands of recipients), you should: Use Vonage Campaigns API for marketing messages (supports opt-out management and scheduling), implement queue-based processing using Bull or BullMQ to avoid overwhelming your API with concurrent requests, add rate limiting logic to respect Vonage's API rate limits (typically 30 – 100 requests/second depending on your account tier), use Promise.all() with batching for parallel sends with controlled concurrency, and store message results in a database for tracking delivery status and failures. Consider Vonage's Bulk SMS or Campaigns API for large-scale messaging needs.

How do I test SMS sending without actually sending messages?

For testing without sending real SMS messages: Mock the Vonage SDK in unit tests using Jest's jest.mock() to simulate successful sends and error conditions, Use Vonage's test credentials (some SMS APIs provide sandbox/test modes, though Vonage primarily uses real credentials), Send to your own phone numbers during development (free trial credits cover testing), Implement a "dry run" mode with an environment variable (SMS_DRY_RUN=true) that logs intended sends without calling the Vonage API, and Use E2E tests with mocked service layer to test controller validation and error handling without actual API calls (see Section 7 for examples).

What are the SMS character limits and encoding types?

Vonage supports two encoding types: GSM-7 (standard English alphabet, numbers, basic symbols) allows 160 characters per single message or 153 characters per segment for concatenated messages (7-byte UDH header overhead), while UCS-2/Unicode (emoji, non-Latin scripts, special characters) allows 70 characters per single message or 67 characters per segment for concatenated messages. Messages automatically use UCS-2 if they contain any unicode characters, which significantly reduces the character limit. To maximize efficiency: avoid emoji in transactional SMS, use GSM-7 characters when possible (check GSM-7 character set), and be aware that some characters like []{}|^€~\\ count as 2 characters in GSM-7 encoding.

How do I deploy this NestJS SMS service to production?

To deploy your NestJS SMS service to production: Use environment-based secrets management (AWS Secrets Manager, Azure Key Vault, Google Secret Manager) instead of .env files, Enable HTTPS using load balancers, reverse proxies (Nginx, Caddy), or platforms with built-in SSL (Heroku, Vercel), Implement rate limiting with @nestjs/throttler to prevent abuse and control costs, Add authentication (JWT, API keys, OAuth) to protect the /sms/send endpoint, Configure structured logging (Winston, Pino) with log aggregation (Datadog, CloudWatch, ELK), Set up monitoring and alerts for SMS failures, rate limit hits, and cost thresholds, Use PM2 or Docker for process management and zero-downtime deployments, and Implement retry logic and dead letter queues for handling transient failures. Consider platforms like AWS ECS, Google Cloud Run, or DigitalOcean App Platform for containerized deployments.


Recap and Next Steps

What You've Built:

You've created a production-ready NestJS application that:

  • Configures and authenticates with the Vonage Messages API using Application ID and private key
  • Implements a dedicated SMS service with proper error handling and logging
  • Exposes a validated API endpoint (POST /sms/send) for sending SMS messages
  • Includes comprehensive security considerations (secrets management, input validation, rate limiting recommendations)
  • Provides unit and E2E testing strategies

Next Steps:

  • Deploy Your Application: Deploy to your preferred platform (AWS, Google Cloud, Azure, Heroku, DigitalOcean, etc.)
  • Implement Webhooks: Set up webhook endpoints to receive delivery receipts and inbound SMS messages (see Vonage Webhooks Documentation)
  • Add Authentication: Protect your /sms/send endpoint with API keys, JWT tokens, or OAuth
  • Enhance Error Handling: Create custom exception filters for specific Vonage error codes
  • Monitor in Production: Set up structured logging and monitoring (Datadog, New Relic, etc.)
  • Optimize Costs: Monitor SMS usage and implement additional rate limiting or user quotas

For more information, consult the Vonage API Documentation and the NestJS Documentation.

Frequently Asked Questions

How to send SMS with NestJS?

You can send SMS messages with NestJS by integrating the Vonage Messages API. Create a NestJS service that uses the Vonage Node.js Server SDK to interact with the API. Expose a POST endpoint in a controller to handle incoming SMS requests and trigger the sending logic implemented in the service.

What is the Vonage Messages API?

The Vonage Messages API is a unified API for sending messages through various channels, including SMS. It's known for its robust features, global reach, and developer-friendly tools like the Node.js Server SDK, making it suitable for applications needing to send notifications or alerts.

Why use NestJS for sending SMS?

NestJS provides a structured and efficient framework for building server-side applications in Node.js. Its modular architecture, dependency injection, and features like validation and configuration management make it ideal for integrating with external APIs like Vonage Messages.

How to set up Vonage for sending SMS?

First, create a Vonage API account. Then, set the Messages API as the default SMS setting in your Vonage dashboard, which is crucial for this process. You'll need to create a Vonage Application, generate keys, enable Messages capability and link your Vonage number. Finally, configure your environment variables in you `.env` file.

What are the prerequisites for this NestJS SMS tutorial?

You'll need Node.js and npm/yarn installed, a Vonage API account (free credits available), and basic knowledge of TypeScript, Node.js, and REST APIs. Familiarity with a terminal/command prompt and optionally Postman or `curl` for testing are helpful.

How to handle Vonage private key securely in NestJS?

Save your `private.key` file in your project root and reference its path in your `.env` file, not directly in code. Add both `.env` and `private.key` to your `.gitignore` to prevent accidental commits. Do not embed the key directly into your codebase. In production, use a proper secrets management solution like AWS Secrets Manager or HashiCorp Vault.

How to validate incoming SMS requests in NestJS?

Use NestJS's ValidationPipe with Data Transfer Objects (DTOs). Create a DTO (e.g., `SendSmsDto`) with class-validator decorators (@IsNotEmpty, @IsPhoneNumber, @Length) to define validation rules for properties like recipient number and message text.

What is the final outcome of this NestJS and Vonage SMS tutorial?

A NestJS application with a POST endpoint (`/sms/send`) that accepts recipient phone number and message text, sending the message via Vonage Messages API. You'll have error handling, logging, and configuration management in place, which are essential for production-ready applications.

When to use Vonage Messages API over other SMS APIs?

Consider Vonage Messages API when you need a unified API across multiple messaging channels, not just SMS. Its robust features, global reach, and well-maintained SDKs are advantages for serious applications.

Can I receive SMS with this setup?

This tutorial covers only *sending* SMS. Receiving SMS and delivery receipts require webhooks, a more advanced topic covered in Vonage documentation but beyond the scope of this guide.

How to implement error handling for the Vonage API in NestJS?

Use try...catch blocks in your service to handle errors from the Vonage SDK. Log these errors with a NestJS Logger and throw specific HttpExceptions at the controller level with appropriate status codes (e.g., 500 Internal Server Error) and detailed error messages for clients.

How to test a NestJS service that uses Vonage Messages API?

For unit tests, mock the ConfigService, Vonage SDK (`vonage.messages.send`), and the 'fs' module using Jest. Test success and failure scenarios for the `sendSms` method. For end-to-end (e2e) tests, use supertest to make real HTTP requests and check responses, including validation errors and service layer errors.

What security measures to consider when sending SMS with Vonage?

Protect your Vonage API credentials, especially the private key. Use robust input validation to prevent injection attacks and malformed data. Implement rate limiting using @nestjs/throttler to prevent abuse and unexpected costs. Add authentication/authorization to your `/sms/send` endpoint to control access.