This guide provides a step-by-step walkthrough for building a production-ready service using Node.js and the NestJS framework to send SMS messages via the Infobip API. We will cover project setup, core implementation, API creation, configuration management, error handling, security considerations, and testing.
By the end of this tutorial, you will have a functional NestJS application capable of accepting API requests to send SMS messages, securely configured using environment variables, and incorporating basic logging and error handling.
Project Overview and Goals
What We're Building:
We will create a simple NestJS application with a single API endpoint. This endpoint will accept a destination phone number and a message text, then use the Infobip Node.js SDK to send the SMS message.
Problem Solved:
This provides a foundational microservice or module for applications needing programmatic SMS capabilities – for notifications, alerts, verification codes, or other communication needs – abstracting the direct interaction with the Infobip API into a reusable service within a standard Node.js framework.
Technologies Used:
- Node.js: The underlying JavaScript runtime environment.
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript. Chosen for its robust structure, dependency injection, modularity, and built-in support for concepts like configuration management and validation.
- TypeScript: Superset of JavaScript adding static types, improving code quality and maintainability.
- Infobip API: The third-party service used for sending SMS messages.
- Infobip Node.js SDK (
@infobip-api/sdk
): Simplifies interaction with the Infobip API by providing pre-built methods and handling authentication. - dotenv: For managing environment variables in development.
System Architecture:
The architecture follows this flow:
- A Client / API Consumer sends an HTTP POST request to the NestJS App.
- The request hits the
/sms/send
endpoint, handled by theSmsController
. - The
SmsController
uses theSmsService
. - The
SmsService
uses theConfigService
to read credentials from the.env
file. - The
SmsService
calls the Infobip Node.js SDK. - The SDK makes an HTTP Request to the Infobip API.
- The Infobip API sends the SMS to the User's Phone.
Prerequisites:
- Node.js: Version 16 or later installed. (Infobip SDK requires v14+, but NestJS benefits from newer versions).
- npm or yarn: Package manager for Node.js.
- Infobip Account: A free trial or paid account is required. You can sign up here.
- Infobip API Key and Base URL: Obtainable from your Infobip account dashboard after signup.
- Verified Phone Number (for Free Trial): If using an Infobip free trial, you can typically only send SMS to the phone number you used during registration.
- Basic understanding of TypeScript, Node.js, REST APIs, and terminal commands.
Final Outcome:
A NestJS application running locally with an endpoint (POST /sms/send
) that successfully sends an SMS via Infobip when provided with valid credentials and recipient details.
1. Setting up the Project
Let's initialize a new NestJS project and install the necessary dependencies.
-
Create a new NestJS Project: Open your terminal and run the NestJS CLI command:
npx @nestjs/cli new nestjs-infobip-sms
When prompted, choose your preferred package manager (npm or yarn). We'll use
npm
in these examples. -
Navigate to Project Directory:
cd nestjs-infobip-sms
-
Install Infobip SDK: Add the official Infobip Node.js SDK to your project:
npm install @infobip-api/sdk
-
Install Configuration Module: NestJS provides a dedicated module for handling environment variables and configuration.
npm install @nestjs/config
(Note:
@nestjs/config
usesdotenv
under the hood). -
Set up Environment Variables: Create a
.env
file in the root of your project. This file will store sensitive credentials and configuration details. Never commit this file to version control.# .env # Infobip Credentials INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL # Application Port (Optional, NestJS defaults to 3000) PORT=3000
- Replace
YOUR_INFOBIP_API_KEY
with the actual API key from your Infobip account. - Replace
YOUR_INFOBIP_BASE_URL
with the specific base URL provided for your account (e.g.,xxxxx.api.infobip.com
).
- Replace
-
Configure the
ConfigModule
: Import and configure theConfigModule
in your main application module (src/app.module.ts
). This makes environment variables accessible throughout your application via theConfigService
.// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { AppController } from './app.controller'; import { AppService } from './app.service'; import { SmsModule } from './sms/sms.module'; // We will create this next @Module({ imports: [ ConfigModule.forRoot({ // Configure the module isGlobal: true, // Make ConfigService available globally envFilePath: '.env', // Specify the env file path }), SmsModule, // Import our future SMS module ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
isGlobal: true
allows injectingConfigService
into any module without needing to importConfigModule
everywhere.envFilePath: '.env'
tells the module where to load the variables from.
-
Project Structure: Your basic project structure will look like this after these steps (NestJS generates some files automatically):
nestjs-infobip-sms/ ├── node_modules/ ├── src/ │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── main.ts │ └── sms/ <-- We will create this module ├── test/ ├── .env <-- Your environment variables ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── nest-cli.json ├── package-lock.json ├── package.json ├── README.md ├── tsconfig.build.json └── tsconfig.json
2. Implementing Core Functionality (SMS Service)
We'll encapsulate the logic for interacting with the Infobip SDK within a dedicated NestJS service.
-
Generate the SMS Module and Service: Use the NestJS CLI to generate a module and a service for SMS functionality:
nest generate module sms nest generate service sms
This creates the
src/sms/
directory withsms.module.ts
andsms.service.ts
(and a spec file). -
Implement the
SmsService
: Opensrc/sms/sms.service.ts
and implement the logic to initialize the Infobip client and send messages.// src/sms/sms.service.ts import { Injectable_ Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip_ AuthType } from '@infobip-api/sdk'; // Import Infobip SDK elements @Injectable() export class SmsService { private readonly logger = new Logger(SmsService.name); private infobipClient: Infobip; constructor(private configService: ConfigService) { // Initialize the Infobip client in the constructor const apiKey = this.configService.get<string>('INFOBIP_API_KEY'); const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL'); if (!apiKey || !baseUrl) { throw new Error('Infobip API Key or Base URL not configured in .env file'); } this.infobipClient = new Infobip({ baseUrl: baseUrl, apiKey: apiKey, authType: AuthType.ApiKey, // Specify API Key authentication }); this.logger.log('Infobip client initialized successfully.'); } /** * Sends an SMS message using the Infobip API. * @param to The destination phone number in international format (e.g., 447123456789). * @param text The content of the SMS message. * @param from (Optional) The sender ID. Check Infobip regulations for your country. * @returns The response from the Infobip API. * @throws Error if the SMS sending fails. */ async sendSms(to: string, text: string, from?: string) { const sender = from || 'InfoSMS'; // Default sender ID if not provided // Basic validation: checks for 10-15 digits. Doesn't validate country codes, leading '+', etc. if (!/^\d{10,15}$/.test(to)) { this.logger.error(`Invalid phone number format: ${to}`); throw new Error(`Invalid destination phone number format.`); } this.logger.log(`Attempting to send SMS to: ${to} from: ${sender}`); try { const response = await this.infobipClient.channels.sms.send({ messages: [ { destinations: [{ to: to }], from: sender, text: text, }, ], }); this.logger.log(`SMS sent successfully via Infobip. Response: ${JSON.stringify(response.data)}`); // Extract relevant info like messageId for potential tracking const message = response.data.messages?.[0]; if (message) { this.logger.log(`Message ID: ${message.messageId}, Status: ${message.status?.name}`); } return response.data; // Return the successful response data } catch (error) { this.logger.error(`Failed to send SMS via Infobip: ${error.message}`, error.stack); // Optionally inspect error details if available from Infobip // Note: The exact path to the error details might vary depending on the type of error returned by Infobip. // Inspect different error responses to ensure robust parsing. const errorResponseData = (error as any)?.response?.data; if (errorResponseData) { this.logger.error(`Infobip Error Details: ${JSON.stringify(errorResponseData)}`); // Throw a more specific error based on Infobip's response if needed const errorText = errorResponseData.requestError?.serviceException?.text || 'Unknown Infobip API error'; throw new Error(`Infobip API Error: ${errorText}`); } throw new Error('Failed to send SMS due to an unexpected error.'); // Generic fallback error } } }
- We inject
ConfigService
to securely retrieve the API key and base URL. - The Infobip client (
Infobip
) is instantiated in the constructor using credentials from the configuration. - The
sendSms
method constructs the payload required by the SDK'schannels.sms.send
function. - Basic logging using NestJS's built-in
Logger
is added for monitoring. - A
try...catch
block handles potential errors during the API call, logging details and throwing an appropriate error. - Includes basic phone number format validation.
- Extracts and logs the
messageId
from the success response, which is useful for tracking.
- We inject
-
Export the Service: Ensure
SmsService
is listed in theproviders
andexports
arrays insrc/sms/sms.module.ts
so it can be injected into other modules (like our future controller).// src/sms/sms.module.ts import { Module } from '@nestjs/common'; import { SmsService } from './sms.service'; // No need to import ConfigModule here if it's global in AppModule @Module({ providers: [SmsService], exports: [SmsService], // Export SmsService }) export class SmsModule {}
3. Building the API Layer (SMS Controller)
Now, let's create an API endpoint to trigger the SMS sending functionality.
-
Generate the SMS Controller:
nest generate controller sms --no-spec
This creates
src/sms/sms.controller.ts
. We add--no-spec
to skip the test file for brevity in this step. -
Install Validation Packages: NestJS uses
class-validator
andclass-transformer
for request validation via Data Transfer Objects (DTOs).npm install class-validator class-transformer
-
Enable Validation Pipe: Globally enable the
ValidationPipe
insrc/main.ts
to automatically validate incoming request bodies against DTOs.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; // Import ValidationPipe and Logger 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 logger = new Logger('Bootstrap'); // Create a logger instance // Enable Validation Pipe Globally app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not defined in DTO transform: true, // Automatically transform payloads to DTO instances forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are present })); const port = configService.get<number>('PORT') || 3000; // Get port from env or default await app.listen(port); logger.log(`Application is running on: ${await app.getUrl()}`); // Use logger } bootstrap();
-
Create a Request DTO: Define a DTO (
Data Transfer Object
) to represent the expected structure and validation rules for the request body. Create a new directorysrc/sms/dto
and a filesrc/sms/dto/send-sms.dto.ts
.// src/sms/dto/send-sms.dto.ts import { IsString, IsNotEmpty, Length, IsOptional, Matches } from 'class-validator'; export class SendSmsDto { @IsString() @IsNotEmpty() // Basic regex: checks for 10-15 digits. Doesn't validate country codes, leading '+', etc. Consider a dedicated library for robust validation. @Matches(/^\d{10,15}$/, { message: 'Phone number must be 10-15 digits and contain only numbers.'}) to: string; @IsString() @IsNotEmpty() @Length(1, 1600) // Set reasonable min/max length for SMS text text: string; @IsString() @IsOptional() // Make the 'from' field optional @Length(1, 11) // Alphanumeric sender IDs are often max 11 chars from?: string; }
- Decorators like
@IsString
,@IsNotEmpty
,@Length
,@Matches
, and@IsOptional
define validation rules.
- Decorators like
-
Implement the
SmsController
: Opensrc/sms/sms.controller.ts
and define the endpoint.// src/sms/sms.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus, Logger, HttpException } from '@nestjs/common'; import { SmsService } from './sms.service'; import { SendSmsDto } from './dto/send-sms.dto'; // Import the DTO @Controller('sms') // Route prefix for this controller export class SmsController { private readonly logger = new Logger(SmsController.name); constructor(private readonly smsService: SmsService) {} @Post('send') // Route: POST /sms/send @HttpCode(HttpStatus.OK) // Return 200 OK on success by default for POST async sendSms(@Body() sendSmsDto: SendSmsDto) { this.logger.log(`Received request to send SMS: ${JSON.stringify(sendSmsDto)}`); try { const result = await this.smsService.sendSms( sendSmsDto.to, sendSmsDto.text, sendSmsDto.from, // Pass optional 'from' field ); // Return a success response, potentially including the messageId return { success: true, message: 'SMS submitted successfully.', details: result, // Include Infobip response details }; } catch (error) { this.logger.error(`Error in sendSms controller: ${error.message}`, error.stack); // Re-throw the error. In production, a global exception filter should be configured // to catch this, log appropriately, and return a standardized error response // without leaking stack traces (as mentioned in Section 5). // For now, re-throwing lets NestJS handle it, often resulting in a 500. // Alternatively, throw a specific HttpException: // throw new HttpException({ // success: false, // message: 'Failed to send SMS.', // error: error.message, // Avoid sending stack trace in production // }, HttpStatus.INTERNAL_SERVER_ERROR); throw error; } } }
- The
@Controller('sms')
decorator sets the base route/sms
. @Post('send')
defines a POST endpoint at/sms/send
.@Body()
decorator tells NestJS to parse the request body and validate it against theSendSmsDto
(thanks to the globalValidationPipe
).- The controller injects
SmsService
and calls itssendSms
method. - It returns a structured success response or re-throws errors for NestJS's exception handling.
- The
-
Import the Controller: Ensure
SmsController
is added to thecontrollers
array 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 providers: [SmsService], exports: [SmsService], }) export class SmsModule {}
4. Integrating with Infobip (Configuration Details)
We've already set up the configuration loading, but let's detail obtaining the credentials.
-
Log in to Infobip: Access your Infobip Portal.
-
Find API Key: Navigate to the API Keys management section. This is often found under your account settings or a dedicated ""Developers"" or ""API"" section. Generate a new API key if you don't have one. Copy the key value.
-
Find Base URL: Your account-specific Base URL is usually displayed prominently on the API documentation landing page within the portal after you log in, or sometimes near the API key management section. It will look something like
xxxxx.api.infobip.com
. Copy this URL. -
Update
.env
: Paste the copied API Key and Base URL into your.env
file created in Step 1.5.# .env INFOBIP_API_KEY=paste_your_api_key_here INFOBIP_BASE_URL=paste_your_base_url_here.api.infobip.com PORT=3000
-
Security: Remember that your
.env
file contains sensitive credentials.- Ensure
.env
is listed in your.gitignore
file (NestJS includes it by default). - Use secure methods for managing environment variables in production environments (e.g., platform-specific secrets management, environment variables set by the deployment system).
- Ensure
5. Error Handling, Logging, and Retries
-
Error Handling: We implemented basic
try...catch
blocks in both the service and controller. The service attempts to parse specific Infobip errors, and the controller either re-throws the error for NestJS's default exception filter (which typically returns a 500 Internal Server Error for unhandled exceptions or specific statuses forHttpException
) or can be customized to return specific error formats. Consider creating custom exception filters in NestJS for more consistent error responses across your application. -
Logging: We use the built-in
Logger
. In a production environment, you would configure more robust logging:- Log Levels: Control verbosity (e.g., only log errors in production, debug logs in development).
- Log Format: Use structured logging (JSON) for easier parsing by log aggregation tools (like Datadog, Splunk, ELK stack).
- Log Destination: Send logs to standard output (for containerized environments), files, or external logging services. NestJS allows replacing the default logger.
-
Retry Mechanisms: For transient network issues or temporary Infobip API unavailability, implementing a retry strategy can improve resilience.
- Simple Retry: Wrap the
infobipClient.channels.sms.send
call in a loop with a delay. - Exponential Backoff: Increase the delay between retries exponentially (e.g., 1s, 2s, 4s, 8s) to avoid overwhelming the API during outages. Libraries like
async-retry
can simplify this. - Caution: Be careful not to retry errors that are clearly not transient (e.g., invalid API key, invalid phone number format, insufficient funds) to avoid unnecessary cost or blocking. Analyze the error response from Infobip before deciding to retry.
Example Snippet Concept (using
async-retry
- requiresnpm install async-retry @types/async-retry
):// Inside SmsService.sendSms method (conceptual) import * as retry from 'async-retry'; // ... inside the method ... try { const response = await retry( async (bail, attemptNumber) => { this.logger.log(`Attempt ${attemptNumber}: Calling Infobip API...`); try { const apiResponse = await this.infobipClient.channels.sms.send({ messages: [ { destinations: [{ to: to }], from: sender, text: text, }, ], }); this.logger.log(`Infobip API call successful on attempt ${attemptNumber}.`); return apiResponse; // Success! Return the result } catch (error) { const err = error as any; // Type assertion for easier access // Check if the error is non-retryable const statusCode = err.response?.status; const errorCode = err.response?.data?.requestError?.serviceException?.messageId; // IMPORTANT: Verify these specific status codes (400, 401) and error strings ('UNAUTHORIZED') // against the official Infobip documentation for errors that should *not* be retried. // This example is illustrative. // Example: Don't retry on Bad Request (400) or Auth errors (401) if (statusCode === 400 || statusCode === 401 || errorCode === 'UNAUTHORIZED') { this.logger.warn(`Non-retryable error encountered (Status: ${statusCode}, Code: ${errorCode}). Bailing out.`); bail(new Error(`Non-retryable Infobip error: ${err.message}`)); // Prevent further retries with a clear message return; // Needed for type checking, bail throws } this.logger.warn(`Retryable error encountered on attempt ${attemptNumber} (Status: ${statusCode || 'Network Error'}, Message: ${err.message}). Retrying...`); throw error; // Throw error to trigger retry } }, { retries: 3, // Number of retries (total attempts = retries + 1) factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial timeout in ms (1 second) maxTimeout: 10000, // Maximum timeout between retries (10 seconds) onRetry: (error, attempt) => { this.logger.warn(`Retrying Infobip API call (Attempt ${attempt}) due to error: ${error.message}`); }, } ); // Process successful response from 'response.data' this.logger.log(`SMS sent successfully via Infobip after retries. Response: ${JSON.stringify(response.data)}`); const message = response.data.messages?.[0]; if (message) { this.logger.log(`Message ID: ${message.messageId}, Status: ${message.status?.name}`); } return response.data; } catch (error) { // Handle final error after retries are exhausted or if bailed out this.logger.error(`Failed to send SMS after all retries: ${error.message}`, error.stack); // Extract details if it's the bailed-out error or the last attempt's error const finalError = error as any; const errorResponseData = finalError.response?.data; if (errorResponseData) { this.logger.error(`Final Infobip Error Details: ${JSON.stringify(errorResponseData)}`); const errorText = errorResponseData.requestError?.serviceException?.text || 'Unknown Infobip API error after retries'; throw new Error(`Infobip API Error: ${errorText}`); } // Re-throw or handle as before throw new Error(`Failed to send SMS after retries: ${error.message}`); }
- Simple Retry: Wrap the
6. Database Schema and Data Layer
For this specific task of sending a single SMS, a database is not strictly required. However, in a real-world application, you would likely integrate a database to:
- Log SMS Messages: Store details of sent messages (recipient, text, timestamp, Infobip
messageId
, status). This is crucial for auditing, tracking, and debugging. - Manage Recipients: Store user profiles or contact lists.
- Track Status Updates: Infobip can send status updates via webhooks. You'd need a database to store these updates against the original message log.
If adding a database (e.g., PostgreSQL with TypeORM):
-
Install Dependencies:
npm install @nestjs/typeorm typeorm pg
-
Configure
TypeOrmModule
: Set up connection details (likely viaConfigService
). -
Define Entities: Create TypeORM entities (e.g.,
SmsLog
) representing your database tables.// Example: src/sms/entities/sms-log.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index, UpdateDateColumn } from 'typeorm'; // Best practice: Define enums in separate files (e.g., src/sms/enums/sms-status.enum.ts) // for better organization and reusability. export enum SmsStatus { PENDING = 'PENDING', SUBMITTED = 'SUBMITTED', // Status after successful API call to Infobip SENT = 'SENT', DELIVERED = 'DELIVERED', FAILED = 'FAILED', REJECTED = 'REJECTED', // Rejected by Infobip or carrier UNDELIVERABLE = 'UNDELIVERABLE' // Number invalid, etc. } @Entity('sms_logs') export class SmsLog { @PrimaryGeneratedColumn('uuid') id: string; @Column({ length: 20 }) // Store recipient number recipient: string; @Column({ length: 11, nullable: true }) // Sender ID used sender?: string; @Column('text') // Full message text messageText: string; @Index() // Index for faster lookups by Infobip ID @Column({ nullable: true, length: 100 }) // Adjust length as needed infobipMessageId?: string; @Index() @Column({ nullable: true, length: 100 }) // If sending batches infobipBulkId?: string; @Index() // Index status for querying undelivered messages etc. @Column({ type: 'enum', enum: SmsStatus, default: SmsStatus.PENDING, // Initial status before submission attempt }) status: SmsStatus; @Column('text', { nullable: true }) // Store failure reason or status details statusReason?: string; @CreateDateColumn({ type: 'timestamp with time zone' }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp with time zone' }) updatedAt: Date; // Track when status was last updated (e.g., via webhook) }
-
Create Repositories: Inject the repository (
@InjectRepository(SmsLog)
) into yourSmsService
to interact with the database (save logs before sending, update status andmessageId
after successful submission, update status via webhook handler). -
Migrations: Use TypeORM migrations to manage schema changes (
typeorm migration:generate -n InitialSchema
,typeorm migration:run
).
7. Security Features
- API Key Security: Already addressed by using environment variables and
.gitignore
. Ensure proper secrets management in production. - Input Validation: Handled by
class-validator
decorators in the DTO and the globalValidationPipe
. This prevents malformed requests and basic injection attempts in the validated fields. - Rate Limiting: Protect your API from abuse and excessive costs. Use a module like
@nestjs/throttler
to limit the number of requests per user or IP address within a specific time window.Configure it innpm install @nestjs/throttler
app.module.ts
and apply the guard globally or to specific controllers/routes. - Authentication/Authorization: This basic endpoint is currently open. In a real application, you would protect it using standard NestJS techniques:
- API Keys: Require clients to send a unique API key in headers, validated by a custom guard.
- JWT (JSON Web Tokens): For user-authenticated sessions. Use
@nestjs/jwt
and@nestjs/passport
. - OAuth2: For third-party application access.
- Helmet: Use the
helmet
middleware (vianpm install helmet
) insrc/main.ts
(app.use(helmet());
) to set various security-related HTTP headers (XSS protection, disabling content sniffing, etc.).// src/main.ts (additions) import helmet from 'helmet'; // ... inside bootstrap() ... app.use(helmet()); // Apply Helmet middleware // ... rest of bootstrap ...
- Sender ID Spoofing: Be aware of regulations regarding Sender IDs (
from
field). Infobip may enforce specific rules depending on the country. Do not allow arbitrary user input for thefrom
field unless strictly controlled and validated.
8. Handling Special Cases
- Phone Number Formats: Infobip requires numbers in international format (e.g.,
447123456789
for UK,14155552671
for US). The basic validation regex (/^\d{10,15}$/
) used earlier is very limited (e.g., it doesn't handle leading '+' signs, variable country code lengths, or specific national number formats). For robust international number handling, using a dedicated library likelibphonenumber-js
(npm install libphonenumber-js
) is strongly recommended for parsing, validation, and formatting based on country codes if your application handles diverse international numbers. You would integrate this validation within the DTO or the service layer. - Character Limits & Encoding: Standard SMS messages have character limits (160 for GSM-7 encoding, fewer for UCS-2 if using non-standard characters like emojis). Longer messages are automatically split into multiple segments by carriers (concatenated SMS), potentially incurring higher costs per message sent. Infobip handles segmentation, but be mindful of message length in your
text
input. The SDK should handle encoding correctly for standard text. You might want to add length validation or warnings based on segment counts. - Sender ID Restrictions: As mentioned, Sender IDs ('from' field) might be restricted (numeric only, require pre-registration, or replaced by a generic number) depending on the destination country's regulations. Test thoroughly and consult Infobip documentation. Your application logic might need to adapt the sender ID based on the destination number.
- Infobip Trial Limitations: Reiterate that free trial accounts can usually only send to the registered/verified phone number and may have other limitations (e.g., on Sender ID usage).
9. Performance Optimizations
For sending single SMS messages on demand, performance bottlenecks are unlikely within this simple service itself. However, if scaling to high volume or bulk sending:
- Bulk Sending: The Infobip API and SDK support sending multiple messages (to different recipients or the same message to many) in a single API call using the
messages
array. This is significantly more efficient than making individual API calls in a loop. Modify theSmsService
and API to accept arrays of recipients/messages.// Conceptual change in SmsService for bulk async sendBulkSms(messages: Array<{ to: string; text: string; from?: string }>) { const infobipMessages = messages.map(msg => ({ destinations: [{ to: msg.to }], from: msg.from || 'InfoSMS', // Use default or provided sender text: msg.text, })); if (infobipMessages.length === 0) { this.logger.warn('sendBulkSms called with empty messages array.'); return { bulkId: null, messages: [] }; // Or throw an error } this.logger.log(`Attempting to send bulk SMS (${infobipMessages.length} messages).`); try { const response = await this.infobipClient.channels.sms.send({ messages: infobipMessages, // Send array of messages }); // Process bulk response (contains bulkId and individual message statuses) this.logger.log(`Bulk SMS submitted successfully. Bulk ID: ${response.data.bulkId}`); // Optionally log individual message statuses from response.data.messages return response.data; } catch (error) { this.logger.error(`Failed to send bulk SMS via Infobip: ${error.message}`, error.stack); // Handle bulk errors (might be partial success/failure) // Re-throw or return structured error info throw error; } }
- Asynchronous Processing: For very high throughput, consider decoupling the API request from the actual SMS sending. The API endpoint could quickly validate the request, perhaps save it to a database with
PENDING
status, and place a job onto a message queue (like RabbitMQ, Kafka, BullMQ). A separate worker service would then consume from the queue, interact with the Infobip API (potentially using bulk sending), handle retries robustly, and update the database status. - Connection Pooling: While the SDK manages underlying HTTP connections, ensure your Node.js application is configured appropriately (e.g.,
UV_THREADPOOL_SIZE
environment variable if needed for other blocking operations, though less relevant for pure network I/O) if performing many concurrent outbound requests or other CPU/IO-intensive tasks.
10. Monitoring, Observability, and Analytics
- Health Checks: Implement a health check endpoint (e.g.,
/health
) using@nestjs/terminus
. This allows load balancers or monitoring systems to verify the service is running and optionally check dependencies (like database connectivity). Checking Infobip reachability might be excessive for a basic health check but could be part of a deeper diagnostic check. - Performance Metrics: Monitor key metrics:
- API request latency (time taken for
/sms/send
endpoint). - API request rate (requests per second/minute).
- Error rates (percentage of 5xx or 4xx responses from your API).
- Infobip API call latency (time taken for
infobipClient.channels.sms.send
). - Infobip API error rates (track errors returned by the SDK/API).
- Queue metrics (if using async processing): queue depth, processing time per message.
- Use Prometheus with a client library (
prom-client
) and expose a/metrics
endpoint, or integrate with APM tools (Datadog APM, New Relic, Dynatrace).
- API request latency (time taken for
- Error Tracking: Use services like Sentry (
@sentry/node
) or equivalent to capture, aggregate, and alert on unhandled exceptions and errors in real-time. Integrate with the NestJS exception filter or logger. - Logging: As discussed in section 5, structured logging forwarded to a central system (ELK, Splunk, Datadog Logs, Grafana Loki, etc.) is essential for troubleshooting and analysis. Log the
messageId
andbulkId
returned by Infobip to correlate application logs with Infobip's delivery reports or webhooks. - Infobip Analytics: Utilize the analytics and reporting features within the Infobip Portal to track delivery rates, costs, and usage patterns. Correlate this data with your application logs using IDs.
11. Troubleshooting and Caveats
- Error:
Unauthorized
/Invalid login details
(Infobip Response)- Cause: Incorrect
INFOBIP_API_KEY
orINFOBIP_BASE_URL
. The Base URL must be the specific one assigned to your account, not a generic one. - Solution: Double-check the values in your
.env
file against those provided in the Infobip portal. Ensure there are no extra spaces or characters. Verify theConfigModule
is loading the.env
file correctly.
- Cause: Incorrect
- Error:
Missing permissions
/Forbidden
(Infobip Response)- Cause: The API key used might not have the necessary permissions to send SMS or use certain features (like specific Sender IDs).
- Solution: Check the permissions associated with the API key in the Infobip portal. Ensure it's enabled and has SMS sending rights.
- Error:
Invalid destination address
- Cause: The
to
phone number format is incorrect or the number itself is invalid/not reachable. - Solution: Ensure the number is in the correct international format (e.g.,
44...
,1...
, without leading+
or00
usually, but check Infobip docs). Implement robust phone number validation (e.g., usinglibphonenumber-js
). Check if the number is valid.
- Cause: The
- SMS Not Received (but API call successful)
- Cause: Trial account limitations (sending only to verified number), incorrect
to
number, carrier filtering/blocking, Sender ID issues (e.g., blocked alphanumeric ID in a country requiring numeric), insufficient funds on Infobip account. - Solution: Verify the
to
number. Check Infobip delivery reports using themessageId
. Test with a known valid number. Review Sender ID rules for the destination country. Check account balance. Contact Infobip support if issues persist.
- Cause: Trial account limitations (sending only to verified number), incorrect
- Environment Variables Not Loaded
- Cause:
.env
file not found at the expected path,ConfigModule
not configured correctly (envFilePath
), variables misspelled in.env
orconfigService.get()
. - Solution: Verify
.env
file location and name. CheckAppModule
configuration forConfigModule.forRoot()
. Ensure variable names match exactly. Add logging in theSmsService
constructor to print the loaded values (remove before production).
- Cause:
- Validation Errors (400 Bad Request from your API)
- Cause: Request body doesn't match the
SendSmsDto
structure or validation rules (@IsString
,@Length
,@Matches
, etc.). - Solution: Check the client request payload. Ensure
Content-Type: application/json
header is sent. Review the DTO validation rules and compare them against the request. The error response from NestJS'sValidationPipe
usually details which fields failed validation.
- Cause: Request body doesn't match the