How to Send SMS with NestJS and Infobip: Complete Tutorial
Learn how to send SMS messages with NestJS and Infobip API. Step-by-step tutorial covering setup, TypeScript implementation, error handling, and production best practices.
Meta Description: Learn how to send SMS messages with NestJS and Infobip API. Step-by-step tutorial covering setup, TypeScript implementation, error handling, and production best practices.
This guide provides a step-by-step walkthrough for building a production-ready SMS service using Node.js and the NestJS framework with Infobip API integration. You'll implement project setup, core functionality, API creation, configuration management, error handling, security features, and testing to send SMS messages programmatically.
By the end of this NestJS SMS tutorial, you'll have a functional application with an API endpoint that sends SMS messages, uses environment variables for secure configuration, and includes logging and error handling.
Project Overview and Goals
What You're Building:
A NestJS application with a single API endpoint that accepts a destination phone number and message text, then uses the Infobip Node.js SDK to send the SMS.
Problem Solved:
This provides a foundational microservice for applications needing programmatic SMS capabilities – notifications, alerts, verification codes, or other communication needs – by abstracting direct Infobip API interaction into a reusable service within a standard Node.js framework.
Technologies Used:
Node.js: The JavaScript runtime environment.
NestJS: A progressive Node.js framework for building efficient, scalable server-side applications using TypeScript. Chosen for its robust structure, dependency injection, modularity, and built-in support for configuration management and validation.
TypeScript: Superset of JavaScript adding static types for improved code quality and maintainability.
Infobip API: The third-party service for sending SMS messages.
Infobip Node.js SDK (@infobip-api/sdk): Simplifies Infobip API interaction with pre-built methods and authentication handling.
dotenv: For managing environment variables in development (included via @nestjs/config).
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 the SmsController.
The SmsController uses the SmsService.
The SmsService uses the ConfigService 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. The Infobip SDK requires Node.js 14 as the minimum, but NestJS performs optimally on Node.js 16+. Verify your version: node --version.
npm or yarn: Package manager for Node.js.
Infobip Account: A free trial or paid account is required. Sign up at Infobip.
Infobip API Key and Base URL: Obtain these from your Infobip account dashboard after signup. Your Base URL is account-specific (e.g., xxxxx.api.infobip.com).
Verified Phone Number (for Free Trial): Free trial accounts can typically send SMS only to the phone number used during registration.
Phone Number Format Requirement: Infobip requires phone numbers in E.164 format without spaces or special characters. Examples: 447123456789 (UK), 14155552671 (US). Numbers must not include +, spaces, or hyphens when passed to the API.
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 NestJS Project for SMS Integration
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:
Bash
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:
Bash
cd nestjs-infobip-sms
Install Infobip SDK:
Add the official Infobip Node.js SDK to your project:
Bash
npm install @infobip-api/sdk
Install Configuration Module:
NestJS provides a dedicated module for handling environment variables and configuration.
Bash
npm install @nestjs/config
(Note: @nestjs/config uses dotenv 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
# .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).
Configure the ConfigModule:
Import and configure the ConfigModule in your main application module (src/app.module.ts). This makes environment variables accessible throughout your application via the ConfigService.
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule } from'@nestjs/config'; // Import ConfigModuleimport { 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 moduleisGlobal: true, // Make ConfigService available globallyenvFilePath: '.env', // Specify the env file path
}),
SmsModule, // Import our future SMS module
],
controllers: [AppController],
providers: [AppService],
})
exportclassAppModule {}
isGlobal: true allows injecting ConfigService into any module without needing to import ConfigModule 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):
2. Implementing Core SMS Functionality with Infobip SDK
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:
Bash
nest generate module sms
nest generate service sms
This creates the src/sms/ directory with sms.module.ts and sms.service.ts (and a spec file).
Implement the SmsService:
Open src/sms/sms.service.ts and implement the logic to initialize the Infobip client and send messages.
TypeScript
// src/sms/sms.service.tsimport { Injectable, Logger } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import { Infobip, AuthType } from'@infobip-api/sdk'; // Import Infobip SDK elements@Injectable()
exportclassSmsService {
privatereadonly logger = newLogger(SmsService.name);
privateinfobipClient: Infobip;
constructor(privateconfigService: ConfigService) {
// Initialize the Infobip client in the constructorconst apiKey = this.configService.get<string>('INFOBIP_API_KEY');
const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL');
if (!apiKey || !baseUrl) {
thrownewError('Infobip API Key or Base URL not configured in .env file');
}
this.infobipClient = newInfobip({
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 E.164 format without symbols (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.
*/asyncsendSms(to: string, text: string, from?: string) {
const sender = from || 'InfoSMS'; // Default sender ID if not provided// Basic validation: checks for 10-15 digits. For production, use libphonenumber-js with max metadata for strict E.164 validation.if (!/^\d{10,15}$/.test(to)) {
this.logger.error(`Invalid phone number format: ${to}`);
thrownewError(`Invalid destination phone number format. Use E.164 format without + or spaces (e.g., 447123456789).`);
}
this.logger.log(`Attempting to send SMS to: ${to} from: ${sender}`);
try {
const response = awaitthis.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 trackingconst 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 asany)?.response?.data;
if (errorResponseData) {
this.logger.error(`Infobip Error Details: ${JSON.stringify(errorResponseData)}`);
// Throw a more specific error based on Infobip's response if neededconst errorText = errorResponseData.requestError?.serviceException?.text || 'Unknown Infobip API error';
thrownewError(`Infobip API Error: ${errorText}`);
}
thrownewError('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's channels.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.
Export the Service:
Ensure SmsService is listed in the providers and exports arrays in src/sms/sms.module.ts so it can be injected into other modules (like our future controller).
TypeScript
// src/sms/sms.module.tsimport { 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
})
exportclassSmsModule {}
3. Building the API Layer (SMS Controller and Routes)
Now, let's create an API endpoint to trigger the SMS sending functionality.
Generate the SMS Controller:
Bash
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 and class-transformer for request validation via Data Transfer Objects (DTOs).
Bash
npm install class-validator class-transformer
Enable Validation Pipe:
Globally enable the ValidationPipe in src/main.ts to automatically validate incoming request bodies against DTOs.
TypeScript
// src/main.tsimport { NestFactory } from'@nestjs/core';
import { AppModule } from'./app.module';
import { ValidationPipe, Logger } from'@nestjs/common'; // Import ValidationPipe and Loggerimport { ConfigService } from'@nestjs/config'; // Import ConfigServiceasyncfunctionbootstrap() {
const app = awaitNestFactory.create(AppModule);
const configService = app.get(ConfigService); // Get ConfigService instanceconst logger = newLogger('Bootstrap'); // Create a logger instance// Enable Validation Pipe Globally
app.useGlobalPipes(newValidationPipe({
whitelist: true, // Strip properties not defined in DTOtransform: true, // Automatically transform payloads to DTO instancesforbidNonWhitelisted: true, // Throw error if non-whitelisted properties are present
}));
const port = configService.get<number>('PORT') || 3000; // Get port from env or defaultawait 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 directory src/sms/dto and a file src/sms/dto/send-sms.dto.ts.
TypeScript
// src/sms/dto/send-sms.dto.tsimport { IsString, IsNotEmpty, Length, IsOptional, Matches } from'class-validator';
exportclassSendSmsDto {
@IsString()
@IsNotEmpty()
// Basic regex: checks for 10-15 digits. For production, use libphonenumber-js parsePhoneNumber() with max metadata for strict E.164 validation.@Matches(/^\d{10,15}$/, { message: 'Phone number must be 10-15 digits and contain only numbers.'})
to: string;
@IsString()
@IsNotEmpty()
@Length(1, 1600) // Maximum length for concatenated SMS (10 segments × 160 chars). Consider tracking segment count for cost control.text: string;
@IsString()
@IsOptional() // Make the 'from' field optional@Length(1, 11) // Alphanumeric sender IDs are typically limited to 11 charactersfrom?: string;
}
Decorators like @IsString, @IsNotEmpty, @Length, @Matches, and @IsOptional define validation rules.
Implement the SmsController:
Open src/sms/sms.controller.ts and define the endpoint.
TypeScript
// src/sms/sms.controller.tsimport { 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 controllerexportclassSmsController {
privatereadonly logger = newLogger(SmsController.name);
constructor(privatereadonlysmsService: SmsService) {}
@Post('send') // Route: POST /sms/send@HttpCode(HttpStatus.OK) // Return 200 OK on success by default for POSTasyncsendSms(@Body() sendSmsDto: SendSmsDto) {
this.logger.log(`Received request to send SMS: ${JSON.stringify(sendSmsDto)}`);
try {
const result = awaitthis.smsService.sendSms(
sendSmsDto.to,
sendSmsDto.text,
sendSmsDto.from, // Pass optional 'from' field
);
// Return a success response, potentially including the messageIdreturn {
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 the SendSmsDto (thanks to the global ValidationPipe).
The controller injects SmsService and calls its sendSms method.
It returns a structured success response or re-throws errors for NestJS's exception handling.
Import the Controller:
Ensure SmsController is added to the controllers array in src/sms/sms.module.ts.
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.
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).
5. Error Handling, Logging, and Retry Strategies
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 for HttpException) 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.
// Inside SmsService.sendSms method (conceptual)import * as retry from'async-retry';
// ... inside the method ...try {
const response = awaitretry(
async (bail, attemptNumber) => {
this.logger.log(`Attempt ${attemptNumber}: Calling Infobip API...`);
try {
const apiResponse = awaitthis.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 asany; // Type assertion for easier access// Check if the error is non-retryableconst 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(newError(`Non-retryable Infobip error: ${err.message}`)); // Prevent further retries with a clear messagereturn; // 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 factorminTimeout: 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 outthis.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 errorconst finalError = error asany;
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';
thrownewError(`Infobip API Error: ${errorText}`);
}
// Re-throw or handle as beforethrownewError(`Failed to send SMS after retries: ${error.message}`);
}
6. Database Schema for SMS Message Logging
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):
// Example: src/sms/entities/sms-log.entity.tsimport { 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.exportenumSmsStatus {
PENDING = 'PENDING',
SUBMITTED = 'SUBMITTED', // Status after successful API call to InfobipSENT = 'SENT',
DELIVERED = 'DELIVERED',
FAILED = 'FAILED',
REJECTED = 'REJECTED', // Rejected by Infobip or carrierUNDELIVERABLE = 'UNDELIVERABLE'// Number invalid, etc.
}
@Entity('sms_logs')
exportclassSmsLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 20 }) // Store recipient numberrecipient: string;
@Column({ length: 11, nullable: true }) // Sender ID usedsender?: string;
@Column('text') // Full message textmessageText: string;
@Index() // Index for faster lookups by Infobip ID@Column({ nullable: true, length: 100 }) // Adjust length as neededinfobipMessageId?: string;
@Index()
@Column({ nullable: true, length: 100 }) // If sending batchesinfobipBulkId?: 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 detailsstatusReason?: 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 your SmsService to interact with the database (save logs before sending, update status and messageId 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 for Production SMS Services
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 global ValidationPipe. 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.
Bash
npm install @nestjs/throttler
Configure it in 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 (via npm install helmet) in src/main.ts (app.use(helmet());) to set various security-related HTTP headers (XSS protection, disabling content sniffing, etc.).
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 the from field unless strictly controlled and validated.
8. Handling Special Cases in SMS Delivery
Phone Number Formats: Infobip requires numbers in E.164 format with digits only – no +, spaces, or hyphens (e.g., 447123456789 for UK, 14155552671 for US). The basic validation regex (/^\d{10,15}$/) used in this guide performs minimal length checking but doesn't validate country codes or national number formats. For production applications handling international numbers, use libphonenumber-js with max metadata (145 KB) for strict validation. Install it with npm install libphonenumber-js and integrate parsing and validation within your DTO or service layer. The max metadata includes precise regular expressions for all countries, while the default min metadata (80 KB) only validates length via isPossible().
Character Limits & Encoding:
GSM-7 encoding: Standard SMS messages support 160 characters when using GSM-7 alphabet (basic Latin characters, numbers, common symbols).
UCS-2 encoding: Messages containing Unicode characters (emojis, non-Latin scripts) use UCS-2 encoding, reducing the limit to 70 characters per message.
Multi-part messages: Longer messages automatically split into segments. GSM-7 messages split at 153 characters per segment; UCS-2 messages split at 67 characters per segment. Carriers charge per segment, potentially increasing costs significantly.
Recommendation: Validate message length in your application and warn users when messages will split into multiple segments. The Infobip SDK handles encoding and segmentation automatically, but you should track segment counts for cost management.
Sender ID Restrictions: Sender IDs (from field) face country-specific regulations. Some countries require numeric-only sender IDs, others allow alphanumeric (typically max 11 characters), and some require pre-registration with carriers. In certain regions, carriers replace custom sender IDs with generic short codes. Test thoroughly for your target countries and consult Infobip's country-specific documentation for sender ID rules. Consider implementing logic to adapt sender IDs based on destination country.
Infobip Trial Limitations: Free trial accounts restrict sending to verified phone numbers only (typically the number used during registration). Trial accounts may also limit sender ID customization and impose daily sending quotas. Upgrade to a paid account for production use.
9. Performance Optimizations for High-Volume SMS
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 the SmsService and API to accept arrays of recipients/messages.
TypeScript
// Conceptual change in SmsService for bulkasyncsendBulkSms(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 sendertext: 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 = awaitthis.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.messagesreturn 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 infothrow 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).
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 and bulkId 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.
Cause: Incorrect INFOBIP_API_KEY or INFOBIP_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 the ConfigModule is loading the .env file correctly.
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 + or 00 usually, but check Infobip docs). Implement robust phone number validation (e.g., using libphonenumber-js). Check if the number is valid.
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 the messageId. Test with a known valid number. Review Sender ID rules for the destination country. Check account balance. Contact Infobip support if issues persist.
Environment Variables Not Loaded
Cause:.env file not found at the expected path, ConfigModule not configured correctly (envFilePath), variables misspelled in .env or configService.get().
Solution: Verify .env file location and name. Check AppModule configuration for ConfigModule.forRoot(). Ensure variable names match exactly. Add logging in the SmsService constructor to print the loaded values (remove before production).
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's ValidationPipe usually details which fields failed validation.
Frequently Asked Questions (FAQ)
Q: What Node.js version do I need for NestJS SMS integration?
A: Node.js 16 or later is recommended. The Infobip SDK requires Node.js 14 minimum, but NestJS performs optimally on Node.js 16+. Verify your version with node --version.
Q: How do I format phone numbers for Infobip API?
A: Use E.164 format with digits only – no +, spaces, or hyphens. Examples: 447123456789 (UK), 14155552671 (US). For production apps, use libphonenumber-js with max metadata for strict validation.
Q: What's the SMS character limit per message?
A: GSM-7 encoding supports 160 characters. Unicode/emojis use UCS-2 encoding with 70 characters. Longer messages split into segments: 153 chars (GSM-7) or 67 chars (UCS-2) per segment. Carriers charge per segment.
Q: Can I send SMS messages for free with Infobip?
A: Infobip offers free trial accounts with limitations – typically restricted to verified phone numbers only. Upgrade to a paid account for production use with full features and higher quotas.
Q: How do I handle SMS delivery failures in NestJS?
A: Implement retry logic with exponential backoff for transient errors. Use the async-retry package and check Infobip error responses to distinguish between retryable (network issues, timeouts) and non-retryable errors (invalid API key, bad phone format).
Q: What's the difference between isPossible() and isValid() phone validation?
A: isPossible() checks phone number length only (available in min metadata, 80 KB). isValid() validates both length and digit patterns using country-specific regular expressions (requires max metadata, 145 KB). Use isValid() for production.
Q: How do I send bulk SMS messages with NestJS?
A: Use Infobip's bulk API by passing an array of messages to channels.sms.send(). This is more efficient than individual API calls. Consider using message queues (RabbitMQ, BullMQ) for high-volume async processing.
Q: Why is my Sender ID not showing correctly?
A: Sender IDs face country-specific regulations. Some countries require numeric-only IDs, others allow alphanumeric (max 11 chars), and some mandate pre-registration. Check Infobip's country documentation for sender ID rules in your target region.
Q: How do I track SMS delivery status?
A: Store the messageId returned by Infobip after sending. Implement webhook endpoints to receive delivery status updates from Infobip. Save these updates to your database (see Section 6 for schema examples).
Q: What security measures should I implement for production?
A: Use environment variables for API keys, implement rate limiting with @nestjs/throttler, add Helmet middleware for HTTP headers, validate all inputs with class-validator, and never expose API keys in client-side code or version control.
Frequently asked questions
Install the Infobip SDK (npm install @infobip-api/sdk) and the NestJS ConfigModule (npm install @nestjs/config). Create a NestJS service to handle sending SMS using the Infobip API and a controller to create an endpoint to trigger the SMS sending functionality. Configure environment variables for your Infobip API key and base URL. Finally, create a DTO for request validation.
The Infobip Node.js SDK (@infobip-api/sdk) simplifies interaction with the Infobip API. It provides pre-built methods for sending SMS messages and handles authentication, reducing the need for direct API calls and simplifying the development process.
NestJS provides a robust, structured framework with features like dependency injection, modularity, and built-in configuration management. This simplifies development, testing, and maintenance of SMS sending services compared to plain Node.js.
Use bulk sending when you need to send multiple messages at once, either to different recipients or the same message to many. It's more efficient than individual API calls, reducing overhead and improving performance, especially for high-volume messaging.
Yes, you can use a free trial Infobip account for initial testing. However, be aware of limitations, such as typically being able to send SMS only to the registered phone number during the trial and possible restrictions on sender IDs.
Implement try-catch blocks in your service and controller to handle errors during API calls. Use NestJS's Logger to log error details and optionally create custom exception filters for consistent error responses. Consider retry mechanisms for transient network issues, using exponential backoff to avoid overwhelming the API.
Protect your API key using environment variables and .gitignore. Validate input using DTOs and a ValidationPipe, implement rate limiting with libraries like @nestjs/throttler, and add authentication/authorization (JWT, API keys, OAuth2) to restrict access.
Infobip expects international format, but basic regex validation isn't sufficient. Use a library like libphonenumber-js for robust international number handling, validation, and formatting, accounting for varying country codes and number formats.
Check for trial account limitations, verify the recipient number, examine Infobip's delivery reports using messageId, test with a known valid number, and review sender ID rules for the destination country. Also check your account balance or contact Infobip support if problems continue.
For high volume, use Infobip's bulk sending feature to send multiple messages in a single API call. Implement asynchronous processing with a message queue for very high throughput, decoupling API requests from actual sending. Consider connection pooling if needed for high concurrency.
While sending a single SMS doesn't strictly require a database, for real-world applications, consider PostgreSQL with TypeORM for storing SMS logs, managing recipients, and tracking status updates via webhooks. Ensure appropriate error handling and security measures are also in place.
Create a .env file in your project's root directory. Add your INFOBIP_API_KEY and INFOBIP_BASE_URL from the Infobip portal. Import and configure the ConfigModule in your app.module.ts to make these environment variables accessible in your NestJS application. Never commit your .env file to version control.
The recommended structure includes a sms module with a service and controller for handling SMS logic and API requests. This modular organization promotes code maintainability and separation of concerns.
Use a library like async-retry to wrap your Infobip API calls. Configure retries with exponential backoff, but avoid retrying non-transient errors like invalid API keys. Ensure you're checking for specific error responses from the API to decide when to retry.
Meta Description: Learn how to send SMS messages with NestJS and Infobip API. Step-by-step tutorial covering setup, TypeScript implementation, error handling, and production best practices.
This guide provides a step-by-step walkthrough for building a production-ready SMS service using Node.js and the NestJS framework with Infobip API integration. You'll implement project setup, core functionality, API creation, configuration management, error handling, security features, and testing to send SMS messages programmatically.
By the end of this NestJS SMS tutorial, you'll have a functional application with an API endpoint that sends SMS messages, uses environment variables for secure configuration, and includes logging and error handling.
Project Overview and Goals
What You're Building:
A NestJS application with a single API endpoint that accepts a destination phone number and message text, then uses the Infobip Node.js SDK to send the SMS.
Problem Solved:
This provides a foundational microservice for applications needing programmatic SMS capabilities – notifications, alerts, verification codes, or other communication needs – by abstracting direct Infobip API interaction into a reusable service within a standard Node.js framework.
Technologies Used:
@infobip-api/sdk): Simplifies Infobip API interaction with pre-built methods and authentication handling.@nestjs/config).System Architecture:
The architecture follows this flow:
/sms/sendendpoint, handled by theSmsController.SmsControlleruses theSmsService.SmsServiceuses theConfigServiceto read credentials from the.envfile.SmsServicecalls the Infobip Node.js SDK.Prerequisites:
node --version.xxxxx.api.infobip.com).447123456789(UK),14155552671(US). Numbers must not include+, spaces, or hyphens when passed to the API.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 NestJS Project for SMS Integration
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:
When prompted, choose your preferred package manager (npm or yarn). We'll use
npmin these examples.Navigate to Project Directory:
cd nestjs-infobip-smsInstall Infobip SDK: Add the official Infobip Node.js SDK to your project:
Install Configuration Module: NestJS provides a dedicated module for handling environment variables and configuration.
(Note:
@nestjs/configusesdotenvunder the hood).Set up Environment Variables: Create a
.envfile in the root of your project. This file will store sensitive credentials and configuration details. Never commit this file to version control.YOUR_INFOBIP_API_KEYwith the actual API key from your Infobip account.YOUR_INFOBIP_BASE_URLwith the specific base URL provided for your account (e.g.,xxxxx.api.infobip.com).Configure the
ConfigModule: Import and configure theConfigModulein 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: trueallows injectingConfigServiceinto any module without needing to importConfigModuleeverywhere.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):
2. Implementing Core SMS Functionality with Infobip SDK
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:
This creates the
src/sms/directory withsms.module.tsandsms.service.ts(and a spec file).Implement the
SmsService: Opensrc/sms/sms.service.tsand 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 E.164 format without symbols (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. For production, use libphonenumber-js with max metadata for strict E.164 validation. if (!/^\d{10,15}$/.test(to)) { this.logger.error(`Invalid phone number format: ${to}`); throw new Error(`Invalid destination phone number format. Use E.164 format without + or spaces (e.g., 447123456789).`); } 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 } } }ConfigServiceto securely retrieve the API key and base URL.Infobip) is instantiated in the constructor using credentials from the configuration.sendSmsmethod constructs the payload required by the SDK'schannels.sms.sendfunction.Loggeris added for monitoring.try...catchblock handles potential errors during the API call, logging details and throwing an appropriate error.messageIdfrom the success response, which is useful for tracking.Export the Service: Ensure
SmsServiceis listed in theprovidersandexportsarrays insrc/sms/sms.module.tsso 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 and Routes)
Now, let's create an API endpoint to trigger the SMS sending functionality.
Generate the SMS Controller:
This creates
src/sms/sms.controller.ts. We add--no-specto skip the test file for brevity in this step.Install Validation Packages: NestJS uses
class-validatorandclass-transformerfor request validation via Data Transfer Objects (DTOs).Enable Validation Pipe: Globally enable the
ValidationPipeinsrc/main.tsto 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/dtoand 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. For production, use libphonenumber-js parsePhoneNumber() with max metadata for strict E.164 validation. @Matches(/^\d{10,15}$/, { message: 'Phone number must be 10-15 digits and contain only numbers.'}) to: string; @IsString() @IsNotEmpty() @Length(1, 1600) // Maximum length for concatenated SMS (10 segments × 160 chars). Consider tracking segment count for cost control. text: string; @IsString() @IsOptional() // Make the 'from' field optional @Length(1, 11) // Alphanumeric sender IDs are typically limited to 11 characters from?: string; }@IsString,@IsNotEmpty,@Length,@Matches, and@IsOptionaldefine validation rules.Implement the
SmsController: Opensrc/sms/sms.controller.tsand 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; } } }@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).SmsServiceand calls itssendSmsmethod.Import the Controller: Ensure
SmsControlleris added to thecontrollersarray 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 API (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.envfile created in Step 1.5.Security: Remember that your
.envfile contains sensitive credentials..envis listed in your.gitignorefile (NestJS includes it by default).5. Error Handling, Logging, and Retry Strategies
Error Handling: We implemented basic
try...catchblocks 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:Retry Mechanisms: For transient network issues or temporary Infobip API unavailability, implementing a retry strategy can improve resilience.
infobipClient.channels.sms.sendcall in a loop with a delay.async-retrycan simplify this.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}`); }6. Database Schema for SMS Message Logging
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:
messageId, status). This is crucial for auditing, tracking, and debugging.If adding a database (e.g., PostgreSQL with TypeORM):
Install Dependencies:
npm install @nestjs/typeorm typeorm pgConfigure
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 yourSmsServiceto interact with the database (save logs before sending, update status andmessageIdafter 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 for Production SMS Services
.gitignore. Ensure proper secrets management in production.class-validatordecorators in the DTO and the globalValidationPipe. This prevents malformed requests and basic injection attempts in the validated fields.@nestjs/throttlerto limit the number of requests per user or IP address within a specific time window.app.module.tsand apply the guard globally or to specific controllers/routes.@nestjs/jwtand@nestjs/passport.helmetmiddleware (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 ...fromfield). Infobip may enforce specific rules depending on the country. Do not allow arbitrary user input for thefromfield unless strictly controlled and validated.8. Handling Special Cases in SMS Delivery
Phone Number Formats: Infobip requires numbers in E.164 format with digits only – no
+, spaces, or hyphens (e.g.,447123456789for UK,14155552671for US). The basic validation regex (/^\d{10,15}$/) used in this guide performs minimal length checking but doesn't validate country codes or national number formats. For production applications handling international numbers, uselibphonenumber-jswithmaxmetadata (145 KB) for strict validation. Install it withnpm install libphonenumber-jsand integrate parsing and validation within your DTO or service layer. Themaxmetadata includes precise regular expressions for all countries, while the defaultminmetadata (80 KB) only validates length viaisPossible().Character Limits & Encoding:
Sender ID Restrictions: Sender IDs (
fromfield) face country-specific regulations. Some countries require numeric-only sender IDs, others allow alphanumeric (typically max 11 characters), and some require pre-registration with carriers. In certain regions, carriers replace custom sender IDs with generic short codes. Test thoroughly for your target countries and consult Infobip's country-specific documentation for sender ID rules. Consider implementing logic to adapt sender IDs based on destination country.Infobip Trial Limitations: Free trial accounts restrict sending to verified phone numbers only (typically the number used during registration). Trial accounts may also limit sender ID customization and impose daily sending quotas. Upgrade to a paid account for production use.
9. Performance Optimizations for High-Volume SMS
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:
messagesarray. This is significantly more efficient than making individual API calls in a loop. Modify theSmsServiceand 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; } }PENDINGstatus, 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.UV_THREADPOOL_SIZEenvironment 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) 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./sms/sendendpoint).infobipClient.channels.sms.send).prom-client) and expose a/metricsendpoint, or integrate with APM tools (Datadog APM, New Relic, Dynatrace).@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.messageIdandbulkIdreturned by Infobip to correlate application logs with Infobip's delivery reports or webhooks.11. Troubleshooting and Common Issues
Unauthorized/Invalid login details(Infobip Response)INFOBIP_API_KEYorINFOBIP_BASE_URL. The Base URL must be the specific one assigned to your account, not a generic one..envfile against those provided in the Infobip portal. Ensure there are no extra spaces or characters. Verify theConfigModuleis loading the.envfile correctly.Missing permissions/Forbidden(Infobip Response)Invalid destination addresstophone number format is incorrect or the number itself is invalid/not reachable.44...,1..., without leading+or00usually, but check Infobip docs). Implement robust phone number validation (e.g., usinglibphonenumber-js). Check if the number is valid.tonumber, carrier filtering/blocking, Sender ID issues (e.g., blocked alphanumeric ID in a country requiring numeric), insufficient funds on Infobip account.tonumber. 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..envfile not found at the expected path,ConfigModulenot configured correctly (envFilePath), variables misspelled in.envorconfigService.get()..envfile location and name. CheckAppModuleconfiguration forConfigModule.forRoot(). Ensure variable names match exactly. Add logging in theSmsServiceconstructor to print the loaded values (remove before production).SendSmsDtostructure or validation rules (@IsString,@Length,@Matches, etc.).Content-Type: application/jsonheader is sent. Review the DTO validation rules and compare them against the request. The error response from NestJS'sValidationPipeusually details which fields failed validation.Frequently Asked Questions (FAQ)
Q: What Node.js version do I need for NestJS SMS integration?
A: Node.js 16 or later is recommended. The Infobip SDK requires Node.js 14 minimum, but NestJS performs optimally on Node.js 16+. Verify your version with
node --version.Q: How do I format phone numbers for Infobip API?
A: Use E.164 format with digits only – no
+, spaces, or hyphens. Examples:447123456789(UK),14155552671(US). For production apps, uselibphonenumber-jswithmaxmetadata for strict validation.Q: What's the SMS character limit per message?
A: GSM-7 encoding supports 160 characters. Unicode/emojis use UCS-2 encoding with 70 characters. Longer messages split into segments: 153 chars (GSM-7) or 67 chars (UCS-2) per segment. Carriers charge per segment.
Q: Can I send SMS messages for free with Infobip?
A: Infobip offers free trial accounts with limitations – typically restricted to verified phone numbers only. Upgrade to a paid account for production use with full features and higher quotas.
Q: How do I handle SMS delivery failures in NestJS?
A: Implement retry logic with exponential backoff for transient errors. Use the
async-retrypackage and check Infobip error responses to distinguish between retryable (network issues, timeouts) and non-retryable errors (invalid API key, bad phone format).Q: What's the difference between isPossible() and isValid() phone validation?
A:
isPossible()checks phone number length only (available inminmetadata, 80 KB).isValid()validates both length and digit patterns using country-specific regular expressions (requiresmaxmetadata, 145 KB). UseisValid()for production.Q: How do I send bulk SMS messages with NestJS?
A: Use Infobip's bulk API by passing an array of messages to
channels.sms.send(). This is more efficient than individual API calls. Consider using message queues (RabbitMQ, BullMQ) for high-volume async processing.Q: Why is my Sender ID not showing correctly?
A: Sender IDs face country-specific regulations. Some countries require numeric-only IDs, others allow alphanumeric (max 11 chars), and some mandate pre-registration. Check Infobip's country documentation for sender ID rules in your target region.
Q: How do I track SMS delivery status?
A: Store the
messageIdreturned by Infobip after sending. Implement webhook endpoints to receive delivery status updates from Infobip. Save these updates to your database (see Section 6 for schema examples).Q: What security measures should I implement for production?
A: Use environment variables for API keys, implement rate limiting with
@nestjs/throttler, add Helmet middleware for HTTP headers, validate all inputs withclass-validator, and never expose API keys in client-side code or version control.