code examples

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

How to Send SMS with Vonage in NestJS: Complete TypeScript Tutorial

Learn how to send SMS messages using Vonage Messages API with NestJS and TypeScript. Step-by-step guide with authentication, error handling, E.164 format validation, rate limiting, and production deployment best practices.

<!-- DEPTH: Introduction lacks specific use case examples and quantifiable benefits (Priority: Medium) -->

Build a production-ready NestJS application capable of sending SMS messages using the Vonage Messages API. This comprehensive tutorial covers everything from project setup and core implementation to error handling, security considerations, and deployment best practices.

You'll create a functional API endpoint that accepts a recipient phone number and a message body, then uses Vonage to deliver the SMS. This solves the common need for applications to send transactional or notification-based text messages programmatically—perfect for OTP verification, alerts, appointment reminders, and customer notifications.

What You'll Build: Project Overview and Goals

<!-- GAP: Missing comparison with alternatives (Twilio, AWS SNS) and why choose Vonage (Type: Enhancement) -->
  • Goal: Create a simple, robust NestJS API endpoint to send SMS messages via Vonage Messages API.
  • Problem Solved: Enable applications to integrate SMS sending capabilities for notifications, alerts, verification codes, or other communications.
  • Technologies:
    • Node.js: JavaScript runtime environment.
    • NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript. Chosen for its structured architecture, dependency injection, and built-in support for modules, controllers, and services, promoting maintainable code.
    • Vonage Messages API: A unified API from Vonage for sending messages across various channels, including SMS. Use this for its reliability and developer-friendly SDK.
    • TypeScript: Superset of JavaScript adding static types, enhancing developer productivity and code quality.
  • Prerequisites:
    • Node.js v20 or later (v22 LTS "Jod" recommended for active support through April 2027). Node.js v18 reached EOL on April 30, 2025 and no longer receives security updates. (Source: Node.js Release Schedule)
    • npm (or yarn) installed with Node.js
    • NestJS CLI v11.x (compatible with Node.js v20+)
    • A Vonage API account (Sign up here)
    • Access to your Vonage API Dashboard to obtain credentials
    • A text editor or IDE (like VS Code)
    • Basic understanding of TypeScript, Node.js, and REST APIs
    • (Optional but Recommended) Vonage CLI installed (npm install -g @vonage/cli)
    • Critical Phone Format Note: Vonage APIs require phone numbers in E.164 format WITHOUT the leading '+' sign. Use format 14155552671 (not +14155552671). This differs from standard E.164 notation and is essential for successful API calls. (Source: Vonage API Support documentation)
  • Final Outcome: A NestJS application with a POST endpoint (/sms/send) that takes a phone number and message, sends the SMS using Vonage, and returns a success or error response.

System Architecture

<!-- DEPTH: Architecture diagram is basic; needs component interaction details and data flow (Priority: Low) -->

The basic flow is straightforward:

[Client Application] --(HTTP POST Request)--> [NestJS API (/sms/send)] | | (Sends SMS via Vonage SDK) v [Vonage Messages API] --(Delivers SMS)--> [Recipient's Phone] | | (Returns Response/Status) v [NestJS API] --(HTTP Response)--> [Client Application]

Step 1: Setting Up Your NestJS Project

<!-- GAP: Missing troubleshooting steps for common setup issues (Type: Substantive) -->

We'll start by creating a new NestJS project using the Nest CLI and installing necessary dependencies.

  1. Install NestJS CLI (if you haven't already):

    bash
    npm install -g @nestjs/cli
  2. Create a new NestJS project: Choose your preferred package manager (npm or yarn) when prompted.

    bash
    nest new vonage-sms-sender
  3. Navigate into the project directory:

    bash
    cd vonage-sms-sender
  4. Install the Vonage Node.js Server SDK: This SDK provides convenient methods for interacting with Vonage APIs. Current version is v3.24.1 (as of 2025).

    bash
    npm install @vonage/server-sdk

    Note: For production deployments, consider pinning to a specific version (e.g., npm install @vonage/server-sdk@3.24.1) to ensure consistent behavior across environments. (Source: npm registry, Vonage GitHub)

  5. Install the NestJS Config module: We'll use this for managing environment variables securely.

    bash
    npm install @nestjs/config

Project Structure and Configuration

<!-- EXPAND: Could benefit from visual tree diagram of final project structure (Type: Enhancement) -->

The NestJS CLI scaffolds a standard project structure:

  • src/: Contains your application source code.
    • main.ts: The application entry point, bootstrapping the NestJS app.
    • app.module.ts: The root module of the application.
    • app.controller.ts: A basic example controller.
    • app.service.ts: A basic example service.
  • .env: (We will create this) File to store environment variables like API keys.
  • tsconfig.json: TypeScript compiler configuration.
  • package.json: Project dependencies and scripts.

Configuration Choice: Using @nestjs/config and a .env file is a standard and secure way to manage sensitive credentials like API keys and application IDs, preventing them from being hardcoded in the source code.

<!-- GAP: Missing explanation of what happens if .env is not found or has wrong format (Type: Substantive) -->

Create the .env file in the project root:

plaintext
# .env

# Vonage Credentials (Get these from your Vonage Dashboard)
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root

# Vonage Sender Number (Must be a Vonage number linked to your app or approved Alphanumeric Sender ID)
VONAGE_FROM_NUMBER=YOUR_VONAGE_NUMBER_OR_SENDER_ID

# Server Port (Optional, defaults usually work)
PORT=3000

Important: Add .env to your .gitignore file to prevent committing sensitive credentials. Create a .env.example file with placeholder values to guide other developers.

Create the .env.example file in the project root:

plaintext
# .env.example

# Vonage Credentials (Replace with your actual values in .env)
VONAGE_APPLICATION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
VONAGE_PRIVATE_KEY_PATH=./private.key

# Vonage Sender Number (Replace with your actual value in .env)
VONAGE_FROM_NUMBER=14155550100

# Server Port (Optional)
PORT=3000

Step 2: Implementing Core SMS Functionality

Let's create a dedicated module and service for handling SMS logic.

  1. Generate an Sms module and service:

    bash
    nest generate module sms
    nest 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.

  2. Configure the Vonage Client in SmsService: Modify src/sms/sms.service.ts to initialize the Vonage client and implement the sendSms method.

    <!-- DEPTH: Code example lacks inline error handling explanation for specific Vonage error codes (Priority: High) -->
    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';
    
    @Injectable()
    export class SmsService {
      private readonly logger = new Logger(SmsService.name);
      private vonage: Vonage;
      private vonageFromNumber: string;
    
      constructor(private configService: ConfigService) {
        // Retrieve Vonage credentials and settings from environment variables
        const applicationId = this.configService.get<string>('VONAGE_APPLICATION_ID');
        const privateKeyPath = this.configService.get<string>('VONAGE_PRIVATE_KEY_PATH');
        this.vonageFromNumber = this.configService.get<string>('VONAGE_FROM_NUMBER');
    
        if (!applicationId || !privateKeyPath || !this.vonageFromNumber) {
          this.logger.error('Vonage configuration missing in environment variables.');
          // In a real app, you might throw an error or handle this more gracefully
          throw new Error('Vonage configuration incomplete.');
        }
    
        // Initialize Vonage client
        // The Vonage SDK reads the private key file content automatically
        this.vonage = new Vonage({
          applicationId: applicationId,
          privateKey: privateKeyPath, // Provide the path to the key file
        });
      }
    
      /**
       * Sends an SMS message using the Vonage Messages API.
       * @param to The recipient's phone number (E.164 format recommended).
       * @param text The message content.
       * @returns The message UUID on success.
       * @throws Error if sending fails.
       */
      async sendSms(to: string, text: string): Promise<string> {
        this.logger.log(`Attempting to send SMS to ${to}`);
    
        const messageRequest: MessageSendRequest = {
          message_type: 'text',
          to: to,
          from: this.vonageFromNumber, // Use the configured sender number/ID
          channel: 'sms',
          text: text,
        };
    
        try {
          const response = await this.vonage.messages.send(messageRequest);
          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);
          // The error object from the SDK might contain more details,
          // especially in `error.response?.data` for specific Vonage API errors.
          // Consider logging error.response?.data if available for deeper debugging.
          // Re-throw a more specific error or handle as needed
          throw new Error(`Vonage API Error: ${error.message}`);
        }
      }
    }

    Why this approach?

    • Dependency Injection: We inject ConfigService to securely access environment variables.
    • Initialization: The Vonage client is initialized in the constructor, ready for use. We check for necessary config values early.
    • Clear Method: The sendSms method encapsulates the logic for sending a single SMS, taking the recipient and message text as arguments.
    • Logging: Basic logging helps track requests and errors.
    • Error Handling: A try...catch block handles potential errors during the API call. The comment now suggests checking nested error properties for more detail.
    • Messages API: We use vonage.messages.send, the recommended method for sending SMS and other message types.
  3. Update SmsModule to provide SmsService: Make sure SmsService is listed in the providers array and exported. Also, import ConfigModule.

    typescript
    // src/sms/sms.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config'; // Import ConfigModule
    import { SmsService } from './sms.service';
    // If you create a controller later, import it here
    // import { SmsController } from './sms.controller';
    
    @Module({
      imports: [ConfigModule], // Make ConfigService available
      providers: [SmsService],
      exports: [SmsService], // Export if needed by other modules
      // controllers: [SmsController], // Uncomment if you add a controller
    })
    export class SmsModule {}
  4. Import SmsModule and ConfigModule in the Root AppModule: Modify src/app.module.ts.

    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';
    import { SmsModule } from './sms/sms.module'; // Import SmsModule
    
    @Module({
      imports: [
        ConfigModule.forRoot({ // Configure ConfigModule globally
          isGlobal: true, // Make config available everywhere
          envFilePath: '.env', // Specify the env file
        }),
        SmsModule, // Import our SmsModule
      ],
      controllers: [AppController], // Keep or remove default controller
      providers: [AppService],     // Keep or remove default service
    })
    export class AppModule {}

    Why .forRoot() and isGlobal: true? This loads the .env file and makes the ConfigService available throughout the application without needing to import ConfigModule in every feature module.

Step 3: Building a Complete API Layer

Now, let's create an API endpoint to trigger the SMS sending functionality.

  1. Generate an Sms controller:

    bash
    nest generate controller sms --no-spec

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

  2. Define the API endpoint and Request Body Validation: We need a way to receive the to phone number and message text in the request. NestJS uses DTOs (Data Transfer Objects) and built-in validation pipes for this.

    • Install validation packages:
      bash
      npm install class-validator class-transformer
    <!-- GAP: Missing explanation of validation decorators and their parameters (Type: Substantive) -->
    • Create a DTO file: Create src/sms/dto/send-sms.dto.ts.

      typescript
      // src/sms/dto/send-sms.dto.ts
      import { IsNotEmpty, IsPhoneNumber, IsString, MaxLength } from 'class-validator';
      
      export class SendSmsDto {
        @IsNotEmpty()
        @IsPhoneNumber(null) // Use null for basic E.164 format check, or specify region code string e.g., 'US'
        readonly to: string;
      
        @IsNotEmpty()
        @IsString()
        @MaxLength(1600) // Vonage allows longer messages, but be mindful of SMS segment costs
        readonly message: string;
      }

      Why DTOs and Validation? DTOs define the expected shape of request data. class-validator decorators automatically validate incoming request bodies against these definitions, ensuring data integrity before it reaches your service logic.

    • Implement the controller: Modify src/sms/sms.controller.ts.

      typescript
      // src/sms/sms.controller.ts
      import { Controller, Post, Body, HttpCode, HttpStatus, Logger } 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') // Route: POST /sms/send
        @HttpCode(HttpStatus.OK) // Send 200 OK on success instead of default 201 Created
        async sendSms(@Body() sendSmsDto: SendSmsDto): Promise<{ success: boolean; messageId?: string; error?: string }> {
          this.logger.log(`Received request to send SMS to ${sendSmsDto.to}`);
          try {
            const messageId = await this.smsService.sendSms(sendSmsDto.to, sendSmsDto.message);
            return { success: true, messageId: messageId };
          } catch (error) {
            this.logger.error(`Error in sendSms controller: ${error.message}`);
            // Return a user-friendly error response
            // Avoid leaking internal error details in production
            return { success: false, error: 'Failed to send SMS. Please try again later.' };
            // Or, consider throwing specific NestJS HttpExceptions for better control
            // throw new HttpException('Failed to send SMS via provider.', HttpStatus.SERVICE_UNAVAILABLE);
          }
        }
      }

      Why this structure?

      • @Controller('sms'): Defines the base route for all methods in this controller.
      • @Post('send'): Maps HTTP POST requests to /sms/send to the sendSms method.
      • @Body(): Tells NestJS to parse the request body and validate it against the SendSmsDto.
      • @HttpCode(HttpStatus.OK): Sets the successful response code to 200.
      • Async/Await: Handles the asynchronous nature of the smsService.sendSms call.
      • Error Handling: Catches errors from the service layer and returns a structured JSON error response.
  3. Add the SmsController to SmsModule: Update src/sms/sms.module.ts.

    typescript
    // src/sms/sms.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config';
    import { SmsService } from './sms.service';
    import { SmsController } from './sms.controller'; // Import the controller
    
    @Module({
      imports: [ConfigModule],
      providers: [SmsService],
      exports: [SmsService],
      controllers: [SmsController], // Add the controller here
    })
    export class SmsModule {}
  4. Enable Validation Pipe Globally: Modify src/main.ts to automatically use the validation pipe for all incoming requests.

    typescript
    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ValidationPipe } from '@nestjs/common'; // Import ValidationPipe
    import { ConfigService } from '@nestjs/config'; // Import ConfigService
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
    
      // Enable global validation pipe
      app.useGlobalPipes(new ValidationPipe({
        whitelist: true, // Strip properties not defined in DTO
        forbidNonWhitelisted: true, // Throw error if extra properties are present
        transform: true, // Automatically transform payloads to DTO instances
      }));
    
      const configService = app.get(ConfigService); // Get ConfigService instance
      const port = configService.get<number>('PORT') || 3000; // Get port from .env or default
    
      await app.listen(port);
      console.log(`Application is running on: ${await app.getUrl()}`);
    }
    bootstrap();

Testing the API Endpoint

<!-- EXPAND: Could add Postman collection example or testing with other tools (Type: Enhancement) -->

Start the development server:

bash
npm run start:dev

You can now send a POST request to http://localhost:3000/sms/send (or your configured port).

Using curl: Replace +14155552671 with a valid E.164 format test number (see Vonage setup) and YOUR_VONAGE_NUMBER_OR_SENDER_ID in your .env.

bash
curl -X POST http://localhost:3000/sms/send \
-H ""Content-Type: application/json"" \
-d '{
  ""to"": ""14155552671"",
  ""message"": ""Hello from NestJS and Vonage!""
}'

Expected Success Response (JSON):

json
{
  ""success"": true,
  ""messageId"": ""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx""
}

Expected Validation Error Response (JSON): (If to is missing or invalid)

json
{
  ""statusCode"": 400,
  ""message"": [
    ""to must be a valid phone number""
    // or ""to should not be empty""
  ],
  ""error"": ""Bad Request""
}

Expected Server Error Response (JSON): (If Vonage API call fails internally)

json
{
  ""success"": false,
  ""error"": ""Failed to send SMS. Please try again later.""
}

Step 4: Configuring Vonage API Credentials

<!-- DEPTH: Vonage setup steps lack screenshots or visual guidance (Priority: Medium) -->

This involves setting up your Vonage account correctly and securely handling credentials.

  1. Log in to your Vonage API Dashboard: https://dashboard.nexmo.com/

  2. Set Messages API as Default:

    • Navigate to Settings in the left-hand menu.
    • Under API settings, find the SMS settings section.
    • Ensure Messages API is selected as the default API for sending SMS messages.
    • Click Save changes.
<!-- GAP: Missing cost estimation and pricing considerations for Vonage SMS (Type: Substantive) -->
  1. Create a Vonage Application:

    • Navigate to Applications > Create a new application.
    • Enter an Application name (e.g., ""NestJS SMS Sender"").
    • Click Generate public and private key. This will automatically download the private.key file. Save this file securely.
    • Copy the generated Application ID.
    • Enable the Messages capability.
      • For sending-only, you can leave the Inbound URL and Status URL blank initially. However, for production apps wanting delivery receipts or two-way messaging, you'd need to configure publicly accessible webhook URLs here (e.g., using ngrok for local development, or your deployed application's URL). Example: https://your-app-domain.com/webhooks/status and https://your-app-domain.com/webhooks/inbound.
    • Scroll down to Link virtual numbers. Click Link next to the Vonage virtual number you want to send from. If you don't have one, you may need to buy one under Numbers > Buy numbers. Alternatively, investigate using Alphanumeric Sender IDs if applicable to your region and use case (requires registration).
    • Click Create application.
  2. Configure Environment Variables (.env):

    • VONAGE_APPLICATION_ID: Paste the Application ID you copied in the previous step.
      • Purpose: Identifies your application to Vonage when using the Messages API with private key authentication.
      • Format: A UUID string (e.g., xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
      • How to obtain: From the Vonage Application details page after creation.
    • VONAGE_PRIVATE_KEY_PATH: Set the path relative to your project root where you saved the downloaded private.key file. We used ./private.key.
      • Purpose: Used by the SDK along with the Application ID to authenticate your API requests securely.
      • Format: A file path string (e.g., ./private.key or config/keys/private.key).
      • How to obtain: Downloaded automatically when generating keys during Vonage Application creation. Keep this file secure and do not commit it to Git.
    • VONAGE_FROM_NUMBER: Enter the Vonage virtual number you linked to the application, or a registered Alphanumeric Sender ID.
      • Purpose: The sender ID displayed on the recipient's phone. Must be a Vonage number owned by you and linked to the application, or an approved Alphanumeric Sender ID.
      • Format: E.164 phone number (e.g., 14155550100) or Alphanumeric string (e.g., MyAppName, max 11 chars, availability/rules vary by country).
      • How to obtain: Purchase/view under Numbers > Your numbers in the Vonage dashboard and link it to your Vonage Application. Or register an Alphanumeric Sender ID via Vonage support/dashboard if available.
    • PORT (Optional): The port your NestJS application will listen on. Defaults usually work locally.
      • Purpose: Network port for the application server.
      • Format: A number (e.g., 3000).
      • How to obtain: Choose any available port.
<!-- GAP: Missing guidance on rotating keys and credential management best practices (Type: Critical) -->
  1. Secure private.key:
    • Ensure the private.key file is included in your .gitignore.
    • In production environments, manage this key file securely (e.g., using secrets management tools provided by your cloud provider or deployment platform). Do not embed it directly in container images.

Step 5: Implementing Error Handling and Logging

Our current setup includes basic logging and try/catch. Let's refine it.

<!-- DEPTH: Error handling section lacks specific Vonage API error code reference table (Priority: High) -->
  • Consistent Error Strategy:

    • The SmsService catches errors from the Vonage SDK and logs detailed information internally.
    • The SmsController catches errors from the service and returns a generic, user-friendly JSON error response ({ success: false, error: '...' }). This prevents leaking internal details.
    • Alternative: For more granular control, throw specific NestJS HttpExceptions from the service or controller (e.g., BadRequestException, ServiceUnavailableException) which NestJS automatically translates into standard HTTP error responses.
  • Logging Levels:

    • NestJS's built-in Logger supports different levels (log, error, warn, debug, verbose).
    • Use log for standard operations (e.g., ""Received request"", ""SMS sent"").
    • Use error for failures (e.g., ""Failed to send SMS"", ""Vonage configuration missing""). Include stack traces where helpful.
    • Use warn for potential issues (e.g., ""Retrying Vonage API call"").
    • Use debug or verbose for detailed diagnostic information during development (can be configured to be disabled in production).
    • Consider using a more robust logging library like Pino (e.g., with nestjs-pino) for production, allowing structured logging (JSON format), log rotation, and easier integration with log analysis tools.
<!-- GAP: Missing concrete retry implementation example with exponential backoff (Type: Substantive) -->
  • Retry Mechanisms:

    • Simple SMS Send: For a basic ""send SMS"" operation, implementing client-side retries can be complex and potentially lead to duplicate messages if the first request succeeded but the response was lost. Vonage itself has internal retry mechanisms for delivery.
    • When to Retry: Client-side retries (ideally with exponential backoff and jitter) might be suitable for transient network errors before the request reaches Vonage, or for critical operations where you need higher assurance (though idempotency handling becomes crucial).
    • Implementation: Libraries like axios-retry (if using Axios directly) or custom logic with setTimeout could be used. For this basic guide, we rely on Vonage's reliability and avoid client-side retries for the send operation itself.
    • Focus on Idempotency: Ensure your triggering mechanism (e.g., the event that causes the SMS to be sent) is idempotent if retries are involved at a higher level in your application.
  • Testing Error Scenarios:

    • Invalid Credentials: Temporarily change VONAGE_APPLICATION_ID or the content of private.key in .env and restart the app. API calls should fail.
    • Invalid 'To' Number: Send a request with a deliberately malformed phone number in the JSON body (e.g., ""to"": ""123""). The ValidationPipe should return a 400 Bad Request.
    • Invalid 'From' Number: Set VONAGE_FROM_NUMBER to a number not linked to your Vonage app or an invalid format. The Vonage API should return an error.
    • Network Issues: Simulate network disruption between your app and Vonage (harder to test locally, might require specific tools or testing in a controlled environment).
    • (Vonage Trial Account Limit): If using a trial account, attempt to send to a number not on your verified list (see Troubleshooting section). This should trigger a specific Vonage error.
  • Log Analysis: During development, monitor the console output where npm run start:dev is running. In production, configure logging to output to files or stream to a log aggregation service (e.g., Datadog, ELK stack, CloudWatch Logs) for centralized monitoring and troubleshooting. Structured JSON logging makes filtering and searching much easier.

Step 6: Database Integration (Optional)

<!-- EXPAND: Could benefit from example schema designs for message tracking (Type: Enhancement) -->

For this specific guide focused only on sending a single SMS via an API call, a database is not strictly required.

However, in a real-world application, you would likely need a database to:

  • Store message history: Keep records of sent messages, their status (using Vonage status webhooks), recipient, content, timestamp, and associated user/event.
  • Manage users: Store user profiles, including phone numbers.
  • Track application state: Relate SMS messages to specific orders, events, or notifications within your application.
<!-- DEPTH: Database section superficial - needs concrete TypeORM/Prisma schema examples (Priority: Medium) -->

If a database were needed:

  • Technology Choice: PostgreSQL, MySQL, MongoDB are common choices with NestJS.
  • ORM/ODM: TypeORM or Prisma are popular choices for interacting with databases in a type-safe way within NestJS. Mongoose is common for MongoDB.
  • Schema: You'd define entities (e.g., User, SmsMessage) with relevant fields (id, to, from, body, status, vonageMessageId, sentAt, updatedAt, potentially userId).
  • Data Layer: Implement repositories or use the ORM's built-in methods within services to create, read, update, and delete data (e.g., saving a record before or after attempting to send the SMS).
  • Migrations: Use the ORM's migration tools (like TypeORM migrations or prisma migrate) to manage database schema changes over time.

For this guide's scope, we will omit database integration.

Step 7: Adding Security Features

Security is paramount, especially when dealing with APIs and external services.

  • Input Validation and Sanitization:
    • Done: We are already using class-validator via ValidationPipe (whitelist: true, forbidNonWhitelisted: true) in main.ts. This ensures incoming request bodies match our SendSmsDto, stripping extra fields and rejecting invalid formats (like non-phone numbers for to).
    • While class-validator handles format validation, explicit sanitization (e.g., stripping potential script tags if messages were user-generated and displayed elsewhere) might be needed depending on the broader application context, though less critical for the SMS content itself being sent out. Libraries like class-sanitizer or custom logic could be used.
<!-- GAP: Missing CORS configuration guidance and HTTPS setup instructions (Type: Critical) -->
  • Protection Against Common Vulnerabilities:

    • Credentials: Securely managing VONAGE_APPLICATION_ID and private.key via .env and ConfigModule prevents exposure in code. Never commit secrets. Use environment variables or dedicated secrets management systems in production.

    • Rate Limiting: Protect the /sms/send endpoint from abuse (e.g., flooding recipients, exhausting your Vonage credit) by implementing rate limiting. Important: Vonage API keys have a default limit of 30 API requests per second (up to 2,592,000 SMS/day). The Messages API has a rate limit of 1 message per second for US destinations. For 10DLC (10-Digit Long Code) US traffic, throughput limits vary by campaign and brand trust score. (Source: Vonage API Support, 2025)

      NestJS has excellent modules for this:

      • Install: npm install --save @nestjs/throttler
      • Configure in app.module.ts:
        typescript
        // src/app.module.ts
        import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
        import { APP_GUARD } from '@nestjs/core'; // Import APP_GUARD
        
        // ... other imports
        
        @Module({
          imports: [
            // ... ConfigModule, SmsModule
            ThrottlerModule.forRoot([{
              ttl: 60000, // Time window in milliseconds (e.g., 60 seconds)
              limit: 10,  // Max requests per window per user/IP
            }]),
          ],
          controllers: [AppController, /* SmsController is part of SmsModule */],
          providers: [
            AppService,
            // Apply ThrottlerGuard globally
            {
              provide: APP_GUARD,
              useClass: ThrottlerGuard,
            },
          ],
        })
        export class AppModule {}
      • This applies a default limit (e.g., 10 requests per minute per IP) to all endpoints. You can customize limits per-controller or per-route using decorators (@Throttle()). Set limits below Vonage's API thresholds to avoid hitting provider rate limits.
    <!-- GAP: Missing JWT/API key authentication implementation examples (Type: Critical) -->
    • Authentication/Authorization: Our current endpoint is public. In a real application, you would protect it. Common methods include:
      • API Keys: Require clients to send a secret API key in headers, validated by a custom Guard.
      • JWT (JSON Web Tokens): For user-specific actions, require a valid JWT obtained after login, validated by @nestjs/jwt and Passport (@nestjs/passport).
      • OAuth: For third-party integrations.
      • This is beyond the scope of a basic SMS sender but crucial for production APIs.
  • Security Headers: Consider adding security headers like helmet (npm install helmet) for protection against common web vulnerabilities (XSS, clickjacking, etc.).

    • Enable in main.ts: app.use(helmet());
<!-- EXPAND: Could add detailed SMS pumping fraud detection patterns and mitigation strategies (Type: Enhancement) -->
  • SMS Pumping Fraud: Be aware of this risk where attackers abuse open SMS endpoints to send messages to premium-rate numbers they control. Rate limiting and authentication are primary defenses. Also consider monitoring usage patterns for anomalies.

  • Testing for Vulnerabilities:

    • Use security scanners (e.g., OWASP ZAP, npm audit/snyk) to check for known vulnerabilities in dependencies.
    • Perform penetration testing, especially if handling sensitive data or integrating into a larger system.
    • Review code for security best practices (input validation, proper authentication/authorization, secure credential handling).

Step 8: Handling International SMS and Special Cases

Sending SMS involves nuances beyond basic text transfer.

  • International Number Formatting:
    • Best Practice: Store and handle phone numbers in E.164 format internally, which includes country code + national number without spaces or special characters.
    • Critical Vonage Requirement: Vonage APIs require E.164 format WITHOUT the leading '+' sign. Standard E.164 notation uses a plus sign (e.g., +14155552671), but Vonage requires 14155552671. This is a key difference from the international standard. (Source: Vonage API Support documentation)
    • Our IsPhoneNumber(null) validator accepts formats with or without the plus sign. For Vonage API calls, ensure you strip the '+' if present before sending to the API.
    • You might need input normalization logic before validation if users enter numbers in local formats. Libraries like google-libphonenumber can help parse, validate, and format numbers for different regions, then strip the '+' for Vonage.
<!-- DEPTH: Alphanumeric sender ID section lacks country-specific availability table (Priority: Medium) -->
  • Alphanumeric Sender IDs:
    • Instead of a phone number, you can sometimes send SMS from a custom string (e.g., ""MyAppName"").
    • Availability: Support varies significantly by country and carrier network. Some countries require pre-registration.
    • Format: Typically 3-11 characters, letters and numbers only (no spaces or special characters).
    • Capability: Usually cannot receive replies.
    • Configuration: If approved and supported for your destination, set VONAGE_FROM_NUMBER in your .env to the Alphanumeric Sender ID.
<!-- GAP: Missing practical cost calculation example for SMS segments and pricing (Type: Substantive) -->
  • Character Limits and Concatenation:
    • A standard SMS segment is 160 GSM-7 characters (or 70 UCS-2 characters for non-Latin alphabets like Cyrillic or Chinese).
    • Longer messages are split into multiple segments (concatenated SMS), which are reassembled by the receiving device.

Frequently Asked Questions (FAQ)

What Node.js version do I need for NestJS SMS integration with Vonage?

You need Node.js v20 or later for NestJS v11. Node.js v18 reached end-of-life on April 30, 2025, and no longer receives security updates. We recommend Node.js v22 LTS "Jod" for active support through April 2027 and compatibility with the latest NestJS and Vonage SDK versions.

What is the correct phone number format for Vonage Messages API?

Vonage Messages API requires phone numbers in E.164 format WITHOUT the leading '+' sign. Use 14155552671 instead of +14155552671. This differs from standard E.164 notation. The format must include the country code followed by the national number, with no spaces or special characters.

How do I get Vonage API credentials for NestJS?

Log in to your Vonage API Dashboard at dashboard.nexmo.com, create a new Application under Applications → Create a new application, and generate a public/private key pair. Copy your Application ID and download the private.key file. Enable the Messages capability and link a virtual phone number to your application. Store these credentials in your .env file.

What are Vonage SMS API rate limits?

Vonage API keys have a default limit of 30 API requests per second (up to 2,592,000 SMS per day). The Messages API has a rate limit of 1 message per second for US destinations. For 10DLC (10-Digit Long Code) US traffic, throughput limits vary by campaign type and brand trust score. Implement rate limiting in your NestJS application to stay below these thresholds.

How do I handle SMS errors in NestJS with Vonage?

Implement try-catch blocks in your service layer to catch Vonage SDK errors. Log detailed error information including error codes and messages for debugging. Return user-friendly error responses from your controller without exposing internal details. Use NestJS's built-in Logger for structured logging and consider throwing specific HttpExceptions (BadRequestException, ServiceUnavailableException) for better error handling.

Can I use TypeScript with Vonage SMS in NestJS?

Yes, NestJS is built with TypeScript as its primary language. The Vonage Node.js SDK v3.24.1 includes TypeScript type definitions, providing full type safety for message requests, responses, and SDK methods. Use DTOs (Data Transfer Objects) with class-validator for type-safe request validation in your NestJS controllers.

How do I send SMS to international numbers with Vonage?

Format international numbers in E.164 format without the '+' sign: country code + national number (e.g., 442071838750 for UK). The Vonage Messages API automatically routes messages internationally. Be aware that pricing varies by destination country. Use libraries like google-libphonenumber to validate and normalize international numbers before sending.

What's the difference between Vonage SMS API and Messages API?

The SMS API uses API Key/Secret authentication and is designed specifically for SMS. The Messages API uses Application ID and private key authentication, supports multiple channels (SMS, MMS, WhatsApp, Viber), and is the recommended modern approach. This tutorial uses the Messages API for better security and future flexibility.

How do I secure my Vonage NestJS SMS endpoint?

Implement multiple security layers: 1) Use @nestjs/throttler for rate limiting (prevent abuse), 2) Add authentication via JWT tokens or API keys using Guards, 3) Validate input with class-validator and ValidationPipe, 4) Secure credentials in environment variables (never commit .env), 5) Use helmet middleware for security headers, 6) Implement CORS restrictions, and 7) Monitor for SMS pumping fraud patterns.

<!-- EXPAND: Could add section on webhook setup for delivery receipts and status tracking (Type: Enhancement) -->

How do I test Vonage SMS locally without sending real messages?

Use the Vonage Messages API sandbox mode which has a limit of 1 message per second and 100 messages per month for testing. Alternatively, set up test numbers in your Vonage dashboard under Numbers → Test numbers for trial accounts. You can also mock the Vonage SDK in your NestJS unit tests using Jest to test your service logic without making actual API calls.

Frequently Asked Questions

How to send SMS with NestJS and Vonage

Set up a new NestJS project, install the Vonage Server SDK and Config module, configure Vonage credentials, create an SMS service and controller, and define a POST route to handle SMS sending. Use the Vonage Messages API to send SMS messages based on requests to your NestJS endpoint. Ensure proper error handling, logging, and validation for production use.

What is the Vonage Messages API used for

The Vonage Messages API is a unified API that allows you to send messages programmatically across multiple channels, including SMS. It offers a developer-friendly way to integrate messaging capabilities into your applications, whether for notifications, alerts, two-factor authentication, or other communication needs.

Why use NestJS for sending SMS messages

NestJS provides a robust and structured framework with features like dependency injection, modules, and controllers. This architecture makes the application more organized, maintainable, and scalable when integrating with external services like the Vonage Messages API.

When to use an Alphanumeric Sender ID with Vonage

Alphanumeric Sender IDs (e.g., 'YourAppName') can replace numeric sender IDs for SMS, but availability depends on the country and carrier. Consider them when branding is crucial, but be aware of potential reply limitations. Registration might be required, and support varies by region.

How to set up Vonage API credentials in NestJS

Store your Vonage Application ID, Private Key Path, and sender number in a `.env` file. Use the `@nestjs/config` module to load these environment variables securely into your NestJS application. Never hardcode API keys directly in your code. Ensure '.env' is in your '.gitignore'.

How to handle Vonage API errors in NestJS

Implement a try-catch block around the Vonage API call in the service layer to handle potential errors. Log the errors for debugging. Return a user-friendly error message to the client in the controller, avoiding exposure of sensitive internal error details.

What is the purpose of a DTO in NestJS SMS sending

A Data Transfer Object (DTO) defines the expected structure of data sent in requests to the SMS API endpoint. DTOs, combined with validation decorators from `class-validator`, ensure data integrity and consistency. Use whitelist and forbidNonWhitelisted options in ValidationPipe for more control over accepted input properties.

How to validate phone numbers in NestJS SMS application

Use the `@IsPhoneNumber` decorator from `class-validator` in your DTO to validate phone numbers. It supports basic E.164 format or region-specific validation. You can also use the `google-libphonenumber` library for more comprehensive number handling.

How to protect my NestJS SMS API against rate limiting issues

Use the `@nestjs/throttler` module. It provides decorators and guards to limit the number of requests to your API endpoint within a specific time window, protecting your application from abuse and excessive charges from the SMS provider.

How to secure my Vonage API private key

Never commit your `private.key` file to version control. Add it to your `.gitignore`. In production, utilize environment variables or a dedicated secret management service offered by your cloud provider or hosting platform.

Why is error handling important in a production SMS API

Robust error handling is crucial. It helps identify issues, aids in debugging, and prevents your application from crashing unexpectedly. Catch errors from the Vonage SDK, log details, return user-friendly responses in controllers, and consider specific HttpException types for granular control.

What are best practices for international phone number handling

Always store and process phone numbers in E.164 format (+14155552671). This international standard ensures consistency and avoids ambiguity. Use appropriate validation to enforce this format. Consider using a library like `google-libphonenumber` to normalize user inputs.

Why does SMS length matter when sending with Vonage

SMS messages are limited to 160 characters (GSM-7 encoding) or 70 characters (UCS-2). Longer messages are broken into segments and reassembled on the recipient's device, which can impact cost. Be mindful of character limits when crafting messages.

Can I send SMS from a custom name instead of a number

Yes, sometimes, using Alphanumeric Sender IDs. Support varies by country and carrier, often requiring registration with Vonage and approval. Check Vonage documentation for regional limitations and guidelines. Replies may not be supported.