code examples
code examples
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
curlfor 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.
-
Install NestJS CLI (Command Line Interface): If you don't have it installed globally, run:
bashnpm install -g @nestjs/cli -
Create NestJS Project: Navigate to your desired parent directory in the terminal and run:
bashnest new vonage-sms-appChoose 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.
-
Navigate to Project Directory:
bashcd vonage-sms-app -
Install Dependencies: Install the Vonage Server SDK and NestJS's configuration module.
bashnpm 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.envfile during development.
Project Structure Overview:
Your initial project structure will look something like this:
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.jsonYou 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.
-
Log in to Vonage Dashboard: Access your Vonage API Dashboard.
-
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.
-
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.
-
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.keyfile. 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.
-
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).
-
Configure Environment Variables:
- Place the
private.keyfile you downloaded into the root directory of yourvonage-sms-appproject. - Create a file named
.envin 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=3000VONAGE_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 theprivate.keyfile 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).
- Place the
-
Update
.gitignore: Ensure your.envfile andprivate.keyare not committed to version control. Add these lines to your.gitignorefile if they aren't already present:text# .gitignore .env private.key node_modules dist -
Load Configuration in NestJS: Modify
src/app.module.tsto load the.envfile usingConfigModule.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 theConfigServiceavailable throughout your application without needing to importConfigModulein 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.
-
Generate SMS Module and Service: Use the NestJS CLI to generate the necessary files:
bashnest generate module sms nest generate service smsThis creates a
src/smsdirectory containingsms.module.tsandsms.service.ts(and spec file for testing). -
Implement
SmsService: Opensrc/sms/sms.service.tsand 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
ConfigServiceto retrieve environment variables. Initializes theVonageSDK instance using the Application ID and the content of the private key file (read usingfs.readFileSync). It includes checks to ensure credentials and the key file exist. sendSmsMethod:- Takes the recipient number (
to) and messagetextas arguments. - Relies on controller-level validation for the
tonumber format. - Constructs the
payloadobject 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 ofmessages, notmessage. - Uses
async/awaitfor cleaner asynchronous code. - Includes logging for success and failure using NestJS's
Logger. - Returns the
message_uuidon success or throws an error on failure.
- Takes the recipient number (
- Constructor: Injects
-
Update
SmsModule: EnsureSmsServiceis 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 {} -
Import
SmsModuleinAppModule: Now uncomment theSmsModuleimport insrc/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.
-
Generate SMS Controller:
bashnest generate controller smsThis creates
src/sms/sms.controller.ts(and spec file for testing). -
Create Request DTO (Data Transfer Object): Create a file
src/sms/dto/send-sms.dto.tsfor validating the incoming request body. Install validation dependencies if you haven't already:bashnpm install class-validator class-transformer # or using yarn: # yarn add class-validator class-transformerNote: 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-validatorto 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 tonullallows 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
- Use decorators from
-
Implement
SmsController: Opensrc/sms/sms.controller.tsand 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 ofSendSmsDto.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 thesendSmsDtoparameter.- Logic: Calls the
smsService.sendSmsmethod. Returns a success response with themessageId. NestJS handles the 201 status. Throws anHttpExceptionwith a 500 status code if the service throws an error.
-
Register Controller in
SmsModule: Uncomment theSmsControllerinsrc/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 {} -
Enable Global Validation Pipe (Optional but Recommended): Instead of applying
@UsePipesto every handler, you can enable theValidationPipeglobally insrc/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 theSmsController.
5. Error Handling and Logging Best Practices
You've already incorporated basic logging and error handling:
- Logging:
Loggerinstances 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
ValidationPipeautomatically handles request validation errors, returning 400 Bad Request responses with details about the validation failures. - Service Errors: The
SmsServicecatches errors from the Vonage SDK (try...catch) and logs them before re-throwing. - Controller Errors: The
SmsControllercatches errors from the service and transforms them into standardHttpExceptionresponses, 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:
- Secrets Management:
- NEVER commit your
.envfile orprivate.keyto 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
.envfiles. Inject these secrets as environment variables into your deployed application. - Restrict file permissions for
private.keyon your server (chmod 400 private.key).
- NEVER commit your
- Input Validation: Already handled by the
ValidationPipeandSendSmsDto. This prevents malformed requests and potential injection issues related to input data.forbidNonWhitelisted: truein the global pipe adds an extra layer of protection against unexpected input fields. - Rate Limiting: To prevent abuse of your SMS endpoint (which costs money), implement rate limiting. The
@nestjs/throttlermodule 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
ttlandlimitas needed.
- Install:
- API Authentication/Authorization: This guide doesn't implement authentication for the
/sms/sendendpoint 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. - 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.
-
Unit Testing (
SmsService):- NestJS generates a spec file (
sms.service.spec.ts). - Use
@nestjs/testingto create a testing module. - Mock dependencies (
ConfigService,VonageSDK,fs). Jest's mocking capabilities are ideal here. - Test the
sendSmsmethod:- Verify it calls
vonage.messages.sendwith the correct payload. - Test success scenarios (mock
sendto resolve successfully). - Test failure scenarios (mock
sendto reject with an error). - Test constructor error handling (e.g., missing key file).
- Verify it calls
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.'); }); }); - NestJS generates a spec file (
-
E2E (End-to-End) Testing (
SmsController):- NestJS generates an e2e spec file in the
testdirectory. - Use
@nestjs/testingandsupertestto make HTTP requests to your running application instance (or a test instance). - Test the
/sms/sendendpoint:- 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.sendSmsto 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); }); }); }); - NestJS generates an e2e spec file in the
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/sendendpoint 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.