Send SMS with NestJS and the Vonage Messages API
This guide provides a step-by-step walkthrough for building a production-ready NestJS application capable of sending SMS messages using the Vonage Messages API. We'll cover project setup, Vonage configuration, service implementation, API endpoint creation, error handling, security, testing, and deployment considerations.
By the end of this tutorial, you will have a functional NestJS service that exposes an API endpoint to send SMS messages reliably via Vonage. This solves the common need for applications to programmatically send notifications, alerts, or one-time passwords (OTPs) via SMS.
Technologies Used:
- Node.js: The JavaScript runtime environment.
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Chosen for its modular architecture, dependency injection, and built-in support for features like validation and configuration management.
- Vonage Messages API: A unified API for sending messages across various channels, including SMS. Chosen for its robust features, global reach, and developer-friendly SDK.
- Vonage Node.js Server SDK: The official SDK for interacting with Vonage APIs from Node.js applications.
- dotenv / @nestjs/config: For managing environment variables securely.
Prerequisites:
- Node.js (LTS version recommended) and npm/yarn installed.
- A Vonage API account (Sign up here). You get free credit to start.
- Access to a terminal or command prompt.
- Basic understanding of TypeScript, Node.js, and REST APIs.
- Optional: Postman or
curl
for testing the API endpoint.
Final Outcome:
A NestJS application with a single POST endpoint (/sms/send
) that accepts a recipient phone number and a message text, sending the message via the Vonage Messages API.
(Note: This guide focuses solely on sending SMS. Receiving SMS or delivery receipts requires setting up webhooks, which is covered in Vonage documentation but beyond the scope of this guide.)
1. Setting up the project
First, we'll create a new NestJS project and install the necessary dependencies.
-
Install NestJS CLI: If you don't have it installed globally, run:
npm install -g @nestjs/cli
-
Create NestJS Project: Navigate to your desired parent directory in the terminal and run:
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.
-
Navigate to Project Directory:
cd vonage-sms-app
-
Install Dependencies: We need the Vonage Server SDK and NestJS's configuration module.
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.@nestjs/config
: For managing environment variables within NestJS.dotenv
: To load environment variables from a.env
file 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.json
We will primarily work within the src
directory, creating new modules, controllers, and services.
2. Configuring Vonage
To interact with the Vonage API, you need credentials and a Vonage phone number configured correctly. We'll use the Vonage Messages API, which requires an Application ID and a private key for 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. You'll need these primarily for using the Vonage CLI or if you were using the older SMS API, but it's good practice to have them noted.
-
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.key
file. Save this file securely – we'll place it in our project root later. The public key is stored by Vonage. - Enable the Messages capability.
- 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 for receiving messages or delivery receipts, but not strictly necessary for just sending. However, Vonage requires valid HTTPS URLs here. - Click ""Generate new application"".
- Note down the Application ID generated for this application.
-
Link a Vonage Number:
- You need a Vonage virtual 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, e.g.,
14155550100
).
-
Configure Environment Variables:
- Place the
private.key
file you downloaded into the root directory of yourvonage-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:
# .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 theprivate.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.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).PORT
: The port your NestJS application will run on.
- Place the
-
Update
.gitignore
: Ensure your.env
file andprivate.key
are not committed to version control. Add these lines to your.gitignore
file if they aren't already present:# .gitignore .env private.key node_modules dist
-
Load Configuration in NestJS: Modify
src/app.module.ts
to load the.env
file usingConfigModule
.// 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'; // We 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 theConfigService
available throughout the application without needing to importConfigModule
in every feature module.envFilePath: '.env'
: Tells the module where to find the environment variables file.
3. Implementing the SMS Service
Now, let's create a dedicated module and service in NestJS to handle the logic for sending SMS messages.
-
Generate SMS Module and Service: Use the NestJS CLI to generate the necessary files:
nest generate module sms nest generate service sms
This creates a
src/sms
directory containingsms.module.ts
andsms.service.ts
(and spec file). -
Implement
SmsService
: Opensrc/sms/sms.service.ts
and implement the logic to initialize the Vonage SDK and send messages.// 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 theVonage
SDK 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. sendSms
Method:- Takes the recipient number (
to
) and messagetext
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 ofmessages
, notmessage
. - 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.
- Takes the recipient number (
- Constructor: Injects
-
Update
SmsModule
: EnsureSmsService
is listed as a provider and exported so it can be used in other modules (like the controller we'll create next).// src/sms/sms.module.ts import { Module } from '@nestjs/common'; import { SmsService } from './sms.service'; // We 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
SmsModule
inAppModule
: Now uncomment theSmsModule
import insrc/app.module.ts
.// 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 API Endpoint
Let's create a controller with an endpoint that accepts POST requests to trigger the SMS sending functionality.
-
Generate SMS Controller:
nest generate controller sms
This creates
src/sms/sms.controller.ts
(and spec file). -
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:npm install class-validator class-transformer # or using yarn: # yarn add class-validator class-transformer
Now, define the DTO:
// 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 (concatenated messages) readonly text: string; }
- We 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 tonull
allows international numbers.@IsString()
: Ensures the field is a string.@Length(1, 1600)
: Ensures the message text has a reasonable length.
- We use decorators from
-
Implement
SmsController
: Opensrc/sms/sms.controller.ts
and define the POST endpoint.// 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 thesendSmsDto
parameter.- Logic: Calls the
smsService.sendSms
method. Returns a success response with themessageId
. NestJS handles the 201 status. Throws anHttpException
with a 500 status code if the service throws an error.
-
Register Controller in
SmsModule
: Uncomment theSmsController
insrc/sms/sms.module.ts
.// 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
@UsePipes
to every handler, you can enable theValidationPipe
globally insrc/main.ts
.// 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 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
We'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 standardHttpException
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), you could implement a custom NestJS Exception Filter. This filter could 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
While this is a simple service, security is paramount:
- Secrets Management:
- NEVER commit your
.env
file orprivate.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
).
- NEVER commit your
- Input Validation: Already handled by the
ValidationPipe
andSendSmsDto
. 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. - 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
:// 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
andlimit
as needed.
- Install:
- API Authentication/Authorization: This guide doesn't implement authentication for the
/sms/send
endpoint itself. In a real-world application, you would 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. - 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 Implementation
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/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).
- Verify it calls
Example Snippet:
// 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 Testing (
SmsController
):- NestJS generates an e2e spec file in the
test
directory. - Use
@nestjs/testing
andsupertest
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):
// 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