code examples
code examples
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.
How to Send SMS with NestJS and Infobip: Complete Tutorial
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/sendendpoint, handled by theSmsController. - The
SmsControlleruses theSmsService. - The
SmsServiceuses theConfigServiceto read credentials from the.envfile. - The
SmsServicecalls 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:
bashnpx @nestjs/cli new nestjs-infobip-smsWhen prompted, choose your preferred package manager (npm or yarn). We'll use
npmin these examples. -
Navigate to Project Directory:
bashcd nestjs-infobip-sms -
Install Infobip SDK: Add the official Infobip Node.js SDK to your project:
bashnpm install @infobip-api/sdk -
Install Configuration Module: NestJS provides a dedicated module for handling environment variables and configuration.
bashnpm install @nestjs/config(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.dotenv# .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_KEYwith the actual API key from your Infobip account. - Replace
YOUR_INFOBIP_BASE_URLwith the specific base URL provided for your account (e.g.,xxxxx.api.infobip.com).
- Replace
-
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.typescript// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { AppController } from './app.controller'; import { AppService } from './app.service'; 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):
textnestjs-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 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:
bashnest generate module sms nest generate service smsThis 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.typescript// 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 } } }- We inject
ConfigServiceto securely retrieve the API key and base URL. - The Infobip client (
Infobip) is instantiated in the constructor using credentials from the configuration. - The
sendSmsmethod constructs the payload required by the SDK'schannels.sms.sendfunction. - Basic logging using NestJS's built-in
Loggeris added for monitoring. - A
try...catchblock handles potential errors during the API call, logging details and throwing an appropriate error. - Includes basic phone number format validation.
- Extracts and logs the
messageIdfrom the success response, which is useful for tracking.
- We inject
-
Export the Service: Ensure
SmsServiceis listed in theprovidersandexportsarrays insrc/sms/sms.module.tsso it can be injected into other modules (like our future controller).typescript// 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:
bashnest generate controller sms --no-specThis 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).bashnpm install class-validator class-transformer -
Enable Validation Pipe: Globally enable the
ValidationPipeinsrc/main.tsto automatically validate incoming request bodies against DTOs.typescript// 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.typescript// 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; }- Decorators like
@IsString,@IsNotEmpty,@Length,@Matches, and@IsOptionaldefine validation rules.
- Decorators like
-
Implement the
SmsController: Opensrc/sms/sms.controller.tsand define the endpoint.typescript// 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
SmsServiceand calls itssendSmsmethod. - It returns a structured success response or re-throws errors for NestJS's exception handling.
- The
-
Import the Controller: Ensure
SmsControlleris added to thecontrollersarray insrc/sms/sms.module.ts.typescript// src/sms/sms.module.ts import { Module } from '@nestjs/common'; import { SmsService } from './sms.service'; import { SmsController } from './sms.controller'; // Import controller @Module({ controllers: [SmsController], // Add controller 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.dotenv# .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
.envfile contains sensitive credentials.- Ensure
.envis listed in your.gitignorefile (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 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:- 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.sendcall 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-retrycan 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):typescript// 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 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):
-
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.typescript// 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
- API Key Security: Already addressed by using environment variables and
.gitignore. Ensure proper secrets management in production. - Input Validation: Handled by
class-validatordecorators 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/throttlerto limit the number of requests per user or IP address within a specific time window.Configure it inbashnpm install @nestjs/throttlerapp.module.tsand 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/jwtand@nestjs/passport. - OAuth2: For third-party application access.
- Helmet: Use the
helmetmiddleware (vianpm install helmet) insrc/main.ts(app.use(helmet());) to set various security-related HTTP headers (XSS protection, disabling content sniffing, etc.).typescript// 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 (
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:
- 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 (
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:
- 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
messagesarray. This is significantly more efficient than making individual API calls in a loop. Modify theSmsServiceand API to accept arrays of recipients/messages.typescript// 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
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. - Connection Pooling: While the SDK manages underlying HTTP connections, ensure your Node.js application is configured appropriately (e.g.,
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 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/sendendpoint). - 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/metricsendpoint, 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
messageIdandbulkIdreturned 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 Common Issues
- Error:
Unauthorized/Invalid login details(Infobip Response)- Cause: Incorrect
INFOBIP_API_KEYorINFOBIP_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
.envfile against those provided in the Infobip portal. Ensure there are no extra spaces or characters. Verify theConfigModuleis loading the.envfile 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
tophone 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+or00usually, 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
tonumber, carrier filtering/blocking, Sender ID issues (e.g., blocked alphanumeric ID in a country requiring numeric), insufficient funds on Infobip account. - Solution: Verify the
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.
- Cause: Trial account limitations (sending only to verified number), incorrect
- Environment Variables Not Loaded
- Cause:
.envfile not found at the expected path,ConfigModulenot configured correctly (envFilePath), variables misspelled in.envorconfigService.get(). - Solution: Verify
.envfile location and name. CheckAppModuleconfiguration forConfigModule.forRoot(). Ensure variable names match exactly. Add logging in theSmsServiceconstructor to print the loaded values (remove before production).
- Cause:
- Validation Errors (400 Bad Request from your API)
- Cause: Request body doesn't match the
SendSmsDtostructure or validation rules (@IsString,@Length,@Matches, etc.). - Solution: Check the client request payload. Ensure
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.
- Cause: Request body doesn't match the
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.