This guide provides a step-by-step walkthrough for integrating the Sinch SMS API into a NestJS application using Node.js. We will build a simple API endpoint capable of sending SMS messages via Sinch, covering project setup, core implementation, security considerations, error handling, testing, and deployment best practices.
By the end of this guide, you will have a functional NestJS service that securely interacts with the Sinch API to send SMS messages, complete with validation, logging, and configuration management. This serves as a robust foundation for incorporating SMS functionality into larger applications.
Project Overview and Goals
Goal: To create a NestJS application with an API endpoint that accepts a phone number and message body, then uses the Sinch API to send an SMS message to that number.
Problem Solved: Provides a structured, reusable, and secure way to integrate transactional or notification-based SMS messaging into Node.js applications built with the NestJS framework.
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.
- Sinch SMS API: The third-party service used for sending SMS messages. We'll use their official Node.js SDK.
- Sinch SDK:
@sinch/sdk-core
,@sinch/sms
(Refer to Sinch documentation for compatible versions)
- Sinch SDK:
- TypeScript: Superset of JavaScript used by NestJS for static typing.
- dotenv: For managing environment variables.
- class-validator / class-transformer: For request payload validation.
System Architecture:
+-----------+ +-----------------+ +-------------+
| | HTTP | | API | |
| Client +-----> | NestJS API +-------> Sinch Cloud |
| (e.g. UI, | Request | (This Guide) | Call | (SMS API) |
| curl) | | | | |
+-----------+ +-------+---------+ +-------------+
|
| Uses Sinch SDK
| Injected Service
v
+---------------+
| Sinch Service |
+---------------+
Prerequisites:
- Node.js (v18 or later recommended) and npm installed.
- A Sinch account with API credentials (Project ID, Key ID, Key Secret).
- A provisioned Sinch phone number capable of sending SMS.
- Basic understanding of TypeScript, Node.js, and REST APIs.
- Access to a terminal or command prompt.
Final Outcome: A NestJS application running locally with a /sms/send
endpoint that successfully sends an SMS via Sinch when provided valid credentials and input.
1. Setting up the Project
Let's initialize our NestJS project and install necessary dependencies.
-
Install NestJS CLI: If you don't have it, install the NestJS command-line interface globally.
npm install -g @nestjs/cli
-
Create New NestJS Project: Generate a new project. Replace
nestjs-sinch-sms
with your desired project name.nest new nestjs-sinch-sms
Select
npm
when prompted for the package manager. -
Navigate to Project Directory:
cd nestjs-sinch-sms
-
Install Dependencies: We need the official Sinch SDK packages, NestJS config module for environment variables, and validation packages.
npm install @sinch/sdk-core @sinch/sms @nestjs/config class-validator class-transformer dotenv
-
Environment Setup (
.env
): Create a.env
file in the project root for storing sensitive credentials.touch .env
Add the following lines to
.env
(leave values blank for now):# .env # Sinch API Credentials (Get from Sinch Dashboard -> Settings -> Access Keys) SINCH_PROJECT_ID= SINCH_KEY_ID= SINCH_KEY_SECRET= # Sinch Phone Number (Get from Sinch Dashboard -> Numbers -> Your Numbers) SINCH_NUMBER= # Sinch API Region (Optional - defaults typically work, check Sinch docs if needed) # e.g., us-1, eu-1 SINCH_REGION=
- Purpose: Using
.env
keeps sensitive keys out of source control and allows for different configurations per environment.@nestjs/config
will load these variables into our application's environment.
- Purpose: Using
-
Configure ConfigModule: Import and configure
ConfigModule
in your main application module (src/app.module.ts
) to load the.env
file globally.// src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { SinchModule } from './sinch/sinch.module'; // We will create this next import { SmsModule } from './sms/sms.module'; // We will create this next import { HealthModule } from './health/health.module'; // Import HealthModule import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; // Import Throttler import { APP_GUARD } from '@nestjs/core'; // Import APP_GUARD @Module({ imports: [ ConfigModule.forRoot({ // Configure ConfigModule isGlobal: true, // Make config available globally envFilePath: '.env', // Specify the env file path }), ThrottlerModule.forRoot([{ // Configure Throttler ttl: 60000, // 60 seconds limit: 10, // 10 requests per IP per ttl }]), SinchModule, // Add SinchModule SmsModule, // Add SmsModule HealthModule, // Add HealthModule ], controllers: [AppController], providers: [ AppService, { // Apply ThrottlerGuard globally provide: APP_GUARD, useClass: ThrottlerGuard, }, ], }) export class AppModule {}
2. Implementing Core Functionality (Sinch Service)
We'll create a dedicated module and service for interacting with the Sinch SDK. This promotes modularity and separation of concerns.
-
Generate Sinch Module and Service: Use the NestJS CLI to scaffold the module and service.
nest generate module sinch nest generate service sinch
This creates
src/sinch/sinch.module.ts
andsrc/sinch/sinch.service.ts
. -
Implement SinchService: Edit
src/sinch/sinch.service.ts
. This service will initialize the Sinch client using credentials from the environment and provide a method to send SMS.// src/sinch/sinch.service.ts import { Injectable, Logger, OnModuleInit, HttpException, HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { SinchClient } from '@sinch/sdk-core'; import { SendBatchRequest } from '@sinch/sms'; @Injectable() export class SinchService implements OnModuleInit { private readonly logger = new Logger(SinchService.name); private sinchClient: SinchClient; private sinchNumber: string; constructor(private configService: ConfigService) {} // OnModuleInit ensures client is initialized after config is ready onModuleInit() { const projectId = this.configService.get<string>('SINCH_PROJECT_ID'); const keyId = this.configService.get<string>('SINCH_KEY_ID'); const keySecret = this.configService.get<string>('SINCH_KEY_SECRET'); this.sinchNumber = this.configService.get<string>('SINCH_NUMBER'); const region = this.configService.get<string>('SINCH_REGION'); // Optional region if (!projectId || !keyId || !keySecret || !this.sinchNumber) { this.logger.error('Missing Sinch credentials or phone number in .env file'); // Throw error during initialization if config is missing throw new Error('Sinch configuration is incomplete. Check .env file.'); } // Initialize the Sinch Client this.sinchClient = new SinchClient({ projectId, keyId, keySecret, ...(region && { smsRegion: region }), }); this.logger.log('Sinch Client Initialized'); } /** * Sends an SMS message using the Sinch API. * @param to The recipient's phone number (E.164 format recommended: e.g., +15551234567). * @param body The text message content. * @returns The result from the Sinch API batches.send operation. * @throws HttpException if sending fails or configuration is missing. */ async sendSms(to: string, body: string): Promise<any> { if (!this.sinchClient) { // This case should ideally be prevented by the OnModuleInit check, but added for safety. this.logger.error('Sinch Client accessed before initialization.'); throw new HttpException('Sinch Client not initialized.', HttpStatus.INTERNAL_SERVER_ERROR); } // Basic validation: Check for '+' prefix for E.164 if (!to.startsWith('+')) { this.logger.warn(`Recipient number '${to}' might not be in E.164 format. Prepending '+' might be needed.`); // Note: This is a rudimentary check; robust E.164 validation/parsing (e.g., using google-libphonenumber) might be needed depending on input sources. } if (!this.sinchNumber.startsWith('+')) { this.logger.warn(`Sinch sender number '${this.sinchNumber}' might not be in E.164 format.`); } const sendBatchRequest: SendBatchRequest = { sendSMSRequestBody: { to: [to], // Expects an array of recipients from: this.sinchNumber, body: body, }, }; this.logger.log(`Attempting to send SMS to ${to}`); try { const response = await this.sinchClient.sms.batches.send(sendBatchRequest); this.logger.log(`SMS sent successfully. Batch ID: ${response.id}`); return response; } catch (error) { this.logger.error(`Failed to send SMS to ${to}: ${error.message}`, error.stack); // Example: Inspecting potential Sinch SDK error structure let statusCode = HttpStatus.INTERNAL_SERVER_ERROR; let message = `Sinch API error: ${error.message}`; // NOTE: The exact structure of the error object from the SDK might vary. // Inspect the actual error object during testing to confirm paths like 'error.response'. const sinchErrorCode = error?.response?.data?.error?.code; const sinchErrorMessage = error?.response?.data?.error?.message; const httpStatusCode = error?.response?.status; // Underlying HTTP status if available if (httpStatusCode === 401) { statusCode = HttpStatus.UNAUTHORIZED; message = 'Sinch authentication failed. Check API credentials.'; } else if (httpStatusCode === 400 || (sinchErrorCode && sinchErrorCode.toString().startsWith('40'))) { // Example mapping for Bad Request statusCode = HttpStatus.BAD_REQUEST; message = `Sinch Bad Request: ${sinchErrorMessage || error.message}`; } // Add more specific mappings based on Sinch error codes (e.g., 403 Forbidden, 503 Service Unavailable) // Re-throw as HttpException for NestJS to handle correctly in the controller layer throw new HttpException(message, statusCode); } } }
- Why
OnModuleInit
? EnsuresConfigService
is ready before initializingSinchClient
. - Why
ConfigService
? Standard NestJS way to access configuration, keeping credentials separate. - Why SDK Client? Abstracts HTTP calls, handles auth, provides type safety.
- Why
HttpException
? Provides standard HTTP error responses that NestJS understands, allowing controllers to handle them gracefully.
- Why
-
Export SinchService: Ensure
SinchService
is exported fromSinchModule
.// src/sinch/sinch.module.ts import { Module } from '@nestjs/common'; import { SinchService } from './sinch.service'; import { ConfigModule } from '@nestjs/config'; // Ensure ConfigModule is available if not global @Module({ imports: [ConfigModule], // Make ConfigService available providers: [SinchService], exports: [SinchService], // Export the service for injection elsewhere }) export class SinchModule {}
3. Building the API Layer
Create the controller and DTO for the SMS sending endpoint.
-
Generate SMS Module and Controller:
nest generate module sms nest generate controller sms
-
Create SendSmsDto: Define the request body structure and validation rules.
mkdir -p src/sms/dto # Create directory if it doesn't exist touch src/sms/dto/send-sms.dto.ts
Add the following content:
// src/sms/dto/send-sms.dto.ts import { IsNotEmpty, IsPhoneNumber, IsString, MaxLength } from 'class-validator'; export class SendSmsDto { @IsNotEmpty() @IsPhoneNumber(null) // Use null for region code to allow any valid E.164 number @IsString() readonly to: string; // Recipient phone number (e.g., +15551234567) @IsNotEmpty() @IsString() @MaxLength(1600) // Sinch allows longer messages, potentially split into multiple parts readonly body: string; // Message content }
- Why DTO? Provides automatic request payload validation via
class-validator
andValidationPipe
.
- Why DTO? Provides automatic request payload validation via
-
Implement SmsController: Inject
SinchService
and create the POST endpoint.// src/sms/sms.controller.ts import { Controller, Post, Body, Logger, HttpException, HttpStatus, HttpCode } from '@nestjs/common'; import { SinchService } from '../sinch/sinch.service'; // Adjust path if needed 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 sinchService: SinchService) {} @Post('send') // Endpoint: POST /sms/send @HttpCode(HttpStatus.CREATED) // Explicitly set success status to 201 Created async sendSms(@Body() sendSmsDto: SendSmsDto) { // ValidationPipe is applied globally (see main.ts) this.logger.log(`Received request to send SMS to: ${sendSmsDto.to}`); try { const result = await this.sinchService.sendSms(sendSmsDto.to, sendSmsDto.body); // Return a success response, including the batch ID from Sinch return { message: 'SMS sending initiated successfully.', batchId: result?.id, // Access the batch ID from the Sinch response status: result?.status // Or other relevant info like initial status }; } catch (error) { this.logger.error(`Error in sendSms endpoint: ${error.message}`, error.stack); // If error is already an HttpException from SinchService, re-throw it if (error instanceof HttpException) { throw error; } // Otherwise, wrap it in a generic server error throw new HttpException( `Failed to send SMS due to an internal error.`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } }
- Why
@HttpCode(HttpStatus.CREATED)
? Explicitly sets the HTTP status code for successful POST requests to 201, aligning with REST conventions and the E2E test expectation. - No Local
@UsePipes
: We rely on the globalValidationPipe
configured inmain.ts
.
- Why
-
Update SmsModule: Import
SinchModule
to makeSinchService
injectable.// src/sms/sms.module.ts import { Module } from '@nestjs/common'; import { SmsController } from './sms.controller'; import { SinchModule } from '../sinch/sinch.module'; // Import SinchModule @Module({ imports: [SinchModule], // Make SinchService available for injection into SmsController controllers: [SmsController], // No providers needed here unless the module had its own services }) export class SmsModule {}
-
Enable Global ValidationPipe: Configure the
ValidationPipe
globally insrc/main.ts
for application-wide automatic validation.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Logger, ValidationPipe } from '@nestjs/common'; // Import ValidationPipe and Logger async function bootstrap() { const app = await NestFactory.create(AppModule); const logger = new Logger('Bootstrap'); // Create a logger instance // Enable global validation pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not defined in DTOs transform: true, // Automatically transform payloads to DTO instances forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are present transformOptions: { enableImplicitConversion: true, // Allow basic type conversions if needed }, })); const port = process.env.PORT || 3000; // Use environment variable for port await app.listen(port); logger.log(`Application listening on port ${port}`); // Log the port } bootstrap();
4. Integrating with Sinch (Credentials)
Retrieve API credentials from Sinch and configure them in the .env
file.
-
Log in to Sinch: Go to the Sinch Customer Dashboard.
-
Find Project ID: Visible on the main dashboard or account details.
-
Generate/Find Access Keys:
- Navigate to Settings -> Access Keys.
- Click Generate new key if needed.
- Copy the
Key ID
andKey Secret
. Store the Key Secret securely as it's shown only once.
-
Find Your Sinch Number:
- Navigate to Numbers -> Your numbers.
- Copy the desired sender number in E.164 format (e.g.,
+12065551212
).
-
Determine Region (Optional but Recommended):
- Check Sinch documentation or account settings for your API region (e.g.,
us-1
,eu-1
). SettingSINCH_REGION
in.env
is good practice.
- Check Sinch documentation or account settings for your API region (e.g.,
-
Update
.env
File: Populate the file with your credentials.# .env SINCH_PROJECT_ID=YOUR_ACTUAL_PROJECT_ID_HERE SINCH_KEY_ID=YOUR_ACTUAL_KEY_ID_HERE SINCH_KEY_SECRET=YOUR_ACTUAL_KEY_SECRET_HERE SINCH_NUMBER=+1xxxxxxxxxx # Your Sinch Number in E.164 format SINCH_REGION=us-1 # Or your specific region, e.g., eu-1
Security: Add .env
to your .gitignore
file to prevent committing secrets.
# .gitignore
# ... other entries
# Environment Variables
.env
*.env
!.env.example # Optional: commit an example file without secrets
# ... other entries
5. Error Handling and Logging
We've incorporated key error handling and logging mechanisms:
- Logging: Using NestJS's
Logger
in services, controllers, and bootstrap for key events and errors. Configure more robust logging (e.g., file transport, external services) for production. - Validation Errors: The global
ValidationPipe
automatically handles invalid request payloads, throwingBadRequestException
(HTTP 400). - Sinch API Errors:
SinchService
catches errors from the SDK, logs them, attempts to interpret common HTTP status codes (like 401 Unauthorized, 400 Bad Request), and throws an appropriateHttpException
. This allows theSmsController
to receive structured errors.- The implementation in
SinchService
(Section 2) already includes refined error handling that maps Sinch errors toHttpException
. You can further customize this mapping based on specific Sinch error codes documented in their API reference.
- The implementation in
- Retry Mechanisms: For transient network issues or temporary Sinch unavailability (e.g., HTTP 503), consider implementing a retry strategy (e.g., using
async-retry
or RxJS operators) withinSinchService.sendSms
. This is an advanced topic not covered here but crucial for high reliability.
6. Database Schema and Data Layer (Optional)
For production systems, store records of sent SMS messages (recipient, timestamp, content, batchId
, delivery status).
This involves:
- Choosing a Database (e.g., PostgreSQL, MongoDB).
- Using an ORM/ODM (e.g., TypeORM, Mongoose).
- Defining a data entity (e.g.,
SmsLog
). - Implementing a data service (e.g.,
SmsLogService
). - Integrating logging calls within
SmsController
orSinchService
.
(Omitted for brevity, focusing on Sinch integration).
7. Security Features
- Input Validation: Handled by
class-validator
andValidationPipe
. - API Key Security: Store keys in
.env
, exclude from Git. Use secure environment variable management in production. - Rate Limiting: Protect against abuse using
@nestjs/throttler
.- Install:
npm install @nestjs/throttler
- Configure in
app.module.ts
(already shown in Section 1, Step 6).
- Install:
- Authentication/Authorization: Protect the
/sms/send
endpoint using standard methods like JWT (@nestjs/jwt
,@nestjs/passport
) or API Keys if the API is exposed externally.
8. Handling Special Cases
- E.164 Phone Number Format: Sinch requires E.164 (
+countrycode...
). TheIsPhoneNumber
validator helps, but robust parsing might be needed for diverse inputs. The warning log inSinchService
provides a basic check. - Message Encoding & Length: Standard SMS (GSM-7) is 160 chars. Non-GSM chars (emojis) use UCS-2 (70 chars/segment). Sinch handles concatenation, but long/special char messages cost more (multiple parts).
MaxLength(1600)
in DTO is generous. - Regional Regulations: Comply with SMS rules (opt-in, timing, sender ID registration like A2P 10DLC in the US) for target regions. Consult Sinch resources.
9. Performance Optimizations
- Async Operations: Leverage Node.js/NestJS async nature with
async/await
for non-blocking I/O. - Sinch Client Initialization: Initialize once in
SinchService
(onModuleInit
) for reuse. - Payload Size: Keep request/response payloads concise.
- Caching: Generally not applicable for unique SMS sends.
High-volume bottlenecks are more likely Sinch rate limits or carrier issues than the NestJS app itself.
10. Monitoring, Observability, and Analytics
- Health Checks: Use
@nestjs/terminus
for a/health
endpoint. - Logging: Implement centralized, structured logging (JSON) for production (ELK, Datadog, etc.).
- Error Tracking: Use services like Sentry (
@sentry/node
,@sentry/nestjs
). - Metrics: Track request rate, latency, error rate (Prometheus/Grafana, APM).
- Sinch Dashboard: Monitor delivery rates, costs, errors via Sinch's platform.
Example Health Check:
- Install:
npm install @nestjs/terminus
- Generate module and controller:
nest generate module health nest generate controller health
- Implement Controller:
// src/health/health.controller.ts import { Controller, Get } from '@nestjs/common'; import { HealthCheckService, HealthCheck } from '@nestjs/terminus'; @Controller('health') export class HealthController { constructor(private health: HealthCheckService) {} @Get() @HealthCheck() check() { // Add specific checks (e.g., database ping, external service reachability) if needed // Example: return this.health.check([() => this.db.pingCheck('database')]); return this.health.check([]); // Basic liveness check } }
- Implement Module:
// src/health/health.module.ts import { Module } from '@nestjs/common'; import { TerminusModule } from '@nestjs/terminus'; import { HealthController } from './health.controller'; @Module({ imports: [TerminusModule], // Import TerminusModule controllers: [HealthController], }) export class HealthModule {}
- Import
HealthModule
intoAppModule
(already shown in Section 1, Step 6).
11. Troubleshooting and Caveats
HTTPError: 401 Unauthorized
: Incorrect Sinch credentials (SINCH_PROJECT_ID
,SINCH_KEY_ID
,SINCH_KEY_SECRET
). Verify.env
against Sinch Dashboard (Settings -> Access Keys). Regenerate keys if needed. Check.env
loading.Invalid number
/Parameter validation failed
(HTTP 400):to
orfrom
number format incorrect (needs E.164:+...
). Sender number (SINCH_NUMBER
) might not be provisioned correctly on Sinch. Verify numbers and formats.- Missing Credentials Error on Startup: Required
SINCH_...
variables missing/empty in.env
. Ensure.env
is populated andConfigModule
is correctly set up. - SDK Version Issues: Ensure
@sinch/sdk-core
and@sinch/sms
versions are compatible. Check Sinch docs. - Region Mismatch: Verify
SINCH_REGION
in.env
matches your account/number region if issues persist. - Sinch Service Outages: Check Sinch Status Page. Implement retries for transient errors.
- Cost: Monitor usage via Sinch Dashboard. Implement rate limiting.
- A2P 10DLC (US): Register brand/campaign via Sinch for US A2P SMS to avoid filtering.
12. Deployment and CI/CD
- Build:
npm run build
(createsdist
folder). - Deploy:
- Copy
dist
,node_modules
(or runnpm ci --omit=dev
on server after copyingdist
,package.json
,package-lock.json
). - Provide production environment variables securely (hosting provider's secrets management, not
.env
file). - Start:
node dist/main.js
. Use PM2 for process management (pm2 start dist/main.js --name my-app
).
- Copy
- CI/CD Pipeline:
- Trigger (e.g., git push).
- Steps: Checkout -> Setup Node ->
npm ci
-> Lint -> Test (npm test
,npm run test:e2e
) -> Build (npm run build
) -> Package (Docker image, zip) -> Deploy -> Inject Env Vars securely -> Restart app.
- Rollback: Strategy to deploy a previous known-good version.
13. Verification and Testing
-
Manual Verification:
-
Populate
.env
with valid credentials and a test recipient number. -
Start:
npm run start:dev
-
Send POST request using
curl
or Postman:Curl Example:
curl --location --request POST 'http://localhost:3000/sms/send' \ --header 'Content-Type: application/json' \ --data-raw '{ ""to"": ""+15551234567"", ""body"": ""Hello from NestJS and Sinch! Test message."" }'
(Replace
+15551234567
with your actual test recipient number)Expected Success Response (Example):
{ ""message"": ""SMS sending initiated successfully."", ""batchId"": ""01ARZ3NDEKTSV4RRFFQ69G5FAV"", ""status"": ""queued"" }
(Actual
batchId
andstatus
may vary) -
Check recipient phone, application logs, and Sinch Dashboard logs.
-
-
Unit Tests: Test components in isolation (mocks for dependencies).
SinchService
Test: MockConfigService
andSinchClient
. VerifysendSms
calls SDK correctly and handles responses/errors.SmsController
Test: MockSinchService
. Verify endpoint calls service method and handles success/errors, returning correct HTTP responses/exceptions.
-
End-to-End (E2E) Tests: Test the full request flow. NestJS CLI sets up
supertest
.// test/app.e2e-spec.ts (Example test cases for the SMS endpoint) import * as request from 'supertest'; import { Test, TestingModule } from '@nestjs/testing'; import { AppModule } from './../src/app.module'; // Adjust path if needed import { INestApplication, ValidationPipe, HttpStatus, HttpException } from '@nestjs/common'; // Import HttpException import { SinchService } from './../src/sinch/sinch.service'; // Adjust path describe('SmsController (e2e)', () => { let app: INestApplication; // Mock the actual Sinch Service to avoid sending real SMS during tests const mockSinchService = { // Mock implementation returns a resolved promise with expected structure sendSms: jest.fn().mockResolvedValue({ id: 'e2e-batch-id', status: 'mock-sent'}), }; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], // Import main application module }) // Override the real SinchService (provided via the imported SinchModule) with our mock .overrideProvider(SinchService) .useValue(mockSinchService) .compile(); app = moduleFixture.createNestApplication(); // Apply the same global validation pipe used in main.ts app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, transformOptions: { // Ensure transformOptions are included if used in main.ts enableImplicitConversion: true, }, })); await app.init(); }); afterAll(async () => { await app.close(); }); // Reset mocks before each test case to ensure test isolation beforeEach(() => { mockSinchService.sendSms.mockClear(); // Reset mock implementation to default success case if needed mockSinchService.sendSms.mockResolvedValue({ id: 'e2e-batch-id', status: 'mock-sent'}); }); it('/sms/send (POST) - should send SMS successfully', () => { const payload = { to: '+19998887777', // Use a valid E.164 format number body: 'E2E Test Message', }; return request(app.getHttpServer()) .post('/sms/send') .send(payload) .expect(HttpStatus.CREATED) // Expect 201 Created (set explicitly in controller) .expect((res) => { expect(res.body.message).toEqual('SMS sending initiated successfully.'); expect(res.body.batchId).toEqual('e2e-batch-id'); expect(mockSinchService.sendSms).toHaveBeenCalledWith(payload.to, payload.body); }); }); it('/sms/send (POST) - should return 400 for invalid phone number format', () => { const payload = { to: 'invalid-number-format', // Invalid E.164 format body: 'E2E Test Message', }; return request(app.getHttpServer()) .post('/sms/send') .send(payload) .expect(HttpStatus.BAD_REQUEST); // Expect 400 Bad Request due to ValidationPipe }); it('/sms/send (POST) - should return 400 for missing message body', () => { const payload = { to: '+19998887777', // 'body' field is missing }; return request(app.getHttpServer()) .post('/sms/send') .send(payload) .expect(HttpStatus.BAD_REQUEST); // Expect 400 Bad Request due to ValidationPipe }); it('/sms/send (POST) - should handle errors from SinchService (e.g., API failure)', () => { const payload = { to: '+19998887777', body: 'E2E Test Message Error Case', }; // Configure the mock service to reject the promise for this test case const errorMessage = 'Mock Sinch API Error'; const errorStatus = HttpStatus.BAD_GATEWAY; // Example error status mockSinchService.sendSms.mockRejectedValue(new HttpException(errorMessage, errorStatus)); // Simulate a specific error return request(app.getHttpServer()) .post('/sms/send') .send(payload) .expect(errorStatus) // Expect the status code thrown by the mocked service .expect((res) => { // NestJS default error response structure expect(res.body.statusCode).toEqual(errorStatus); expect(res.body.message).toEqual(errorMessage); }); }); });