Building a Production-Ready Infobip SMS Service with Node.js and NestJS
Complete guide to building an Infobip SMS integration with Node.js and NestJS. Learn API authentication, phone number validation, error handling, and deployment best practices for production messaging services.
This comprehensive guide walks you through building a production-ready SMS service using Node.js, the NestJS framework, and the Infobip API. You'll learn how to implement secure API authentication, robust phone number validation using E.164 standards, comprehensive error handling, and deployment strategies for reliable SMS delivery—essential for marketing campaigns, transactional messaging, and two-factor authentication systems.
We'll use the official Infobip Node.js SDK (@infobip-api/sdk) for seamless integration. By the end, you'll have a fully deployable NestJS module with logging, validation, configuration management, and best practices for production environments.
Project Overview: Building a Scalable SMS Microservice
This guide provides a complete walkthrough for building a robust service using Node.js and the NestJS framework to send SMS messages via the Infobip API. We'll cover everything from project setup and core implementation to error handling, security, deployment, and monitoring, enabling you to integrate reliable SMS functionality into your applications, often a key component of marketing campaigns.
We'll focus on using the official Infobip Node.js SDK for seamless integration. By the end, you'll have a deployable NestJS module capable of sending SMS messages, complete with logging, validation, and configuration management.
Project Overview and Goals
What We'll Build:
A NestJS microservice or module with a dedicated API endpoint (/infobip/sms/send) that accepts requests to send SMS messages. This service will securely interact with the Infobip API using their official Node.js SDK.
Problem Solved:
Provides a centralized, scalable, and maintainable way to handle SMS sending logic within a larger application ecosystem. Decouples SMS functionality from other business logic, making the system easier to manage and test. Enables programmatic sending of SMS for notifications, alerts, 2FA, or as part of marketing campaign execution flows.
Technologies Used:
Node.js: The underlying JavaScript runtime environment.
NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript. Chosen for its modular architecture, dependency injection, and built-in support for best practices.
TypeScript: Superset of JavaScript adding static types for better code quality and maintainability.
Infobip API & Node.js SDK (@infobip-api/sdk): The official library for interacting with Infobip's communication platform APIs. Simplifies authentication and API calls.
Docker (Optional): For containerizing the application for consistent deployment.
System Architecture:
Diagram Placeholder: Client -> NestJS App -> Infobip Service -> Infobip API, with optional logging to a Database
Prerequisites:
A free or paid Infobip account.
Node.js (v14 or higher, minimum version required by @infobip-api/sdk as of January 2025).
Basic understanding of TypeScript, Node.js, and REST APIs.
Access to a terminal or command prompt.
Git (recommended for version control).
Docker (optional, for containerized deployment).
Expected Outcome:
A functional NestJS application with an endpoint to send SMS messages via Infobip, incorporating best practices for configuration, error handling, validation, and logging.
1. Setting up the Project
Let's initialize our NestJS project and install the necessary dependencies.
Create NestJS Project:
Open your terminal and run the NestJS CLI command:
Bash
nest new infobip-sms-service
cd infobip-sms-service
Choose your preferred package manager (npm or yarn) when prompted.
Install Dependencies:
We need the Infobip SDK, a configuration module, and validation libraries.
Bash
# Using npm
npm install @infobip-api/sdk @nestjs/config class-validator class-transformer
# Using yarn
yarn add @infobip-api/sdk @nestjs/config class-validator class-transformer
@infobip-api/sdk: The official Infobip SDK.
@nestjs/config: For managing environment variables securely.
class-validator & class-transformer: For validating incoming request data (DTOs).
Configure Environment Variables:
NestJS promotes using environment variables for configuration. Create a .env file in the project root:
INFOBIP_API_KEY: Obtain this from your Infobip account dashboard (usually under API Keys).
INFOBIP_BASE_URL: Find this on your Infobip account dashboard (it's specific to your account, usually displayed prominently on the main dashboard page after login, e.g., xxxxx.api.infobip.com).
Important: Add .env to your .gitignore file to prevent committing sensitive credentials.
Text
# .gitignore (ensure this line exists or add it)
.env
Load Environment Variables:
Modify src/app.module.ts to load the .env file using @nestjs/config.
Create the Infobip Module:
Organize Infobip-related logic into its own module.
Bash
nest generate module infobip
nest generate service infobip --no-spec # Optional: --no-spec skips test file
nest generate controller infobip --no-spec
This creates src/infobip/infobip.module.ts, src/infobip/infobip.service.ts, and src/infobip/infobip.controller.ts. Ensure InfobipModule is imported in AppModule as shown above.
The InfobipService will encapsulate the logic for interacting with the Infobip SDK.
Initialize Infobip Client:
Inject ConfigService to access environment variables and initialize the Infobip client instance.
TypeScript
// src/infobip/infobip.service.tsimport { Injectable, Logger, InternalServerErrorException, BadRequestException } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import { Infobip, AuthType } from'@infobip-api/sdk'; // Import Infobip SDK elements@Injectable()
exportclassInfobipService {
privatereadonly logger = newLogger(InfobipService.name);
privateinfobipClient: Infobip;
constructor(privateconfigService: ConfigService) {
const apiKey = this.configService.get<string>('INFOBIP_API_KEY');
const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL');
if (!apiKey || !baseUrl) {
this.logger.error('Infobip API Key or Base URL not configured in environment variables.');
thrownewInternalServerErrorException('Infobip configuration missing.');
}
// Initialize the Infobip clientthis.infobipClient = newInfobip({
baseUrl: baseUrl,
apiKey: apiKey,
authType: AuthType.ApiKey, // Specify API Key authentication
});
this.logger.log('Infobip client initialized successfully.');
}
// Method to send SMS will go here
}
Implement SMS Sending Logic:
Create a method to handle sending SMS messages. This method takes the destination number, message text, and optionally the sender ID.
TypeScript
// src/infobip/infobip.service.ts// ... (imports and constructor from above) ...exportclassInfobipService {
// ... (logger, infobipClient, constructor) ...asyncsendSms(to: string, text: string, from?: string) {
this.logger.log(`Attempting to send SMS to: ${to}`);
// Basic validation (more robust validation in DTO later)if (!to || !text) {
thrownewBadRequestException('Destination number (to) and text message are required.');
}
// WARNING: Basic regex check for phone number format.// This check is insufficient for reliable international format validation (e.g., doesn't enforce '+')// and may allow invalid numbers. Production applications should use a dedicated library like libphonenumber-js// for robust parsing and validation.if (!/^\d{10,15}$/.test(to.replace(/^\+/, ''))) { // Allow optional '+' at start but otherwise just check digitsthis.logger.warn(`Potentially invalid phone number format detected by basic check for 'to': ${to}. Consider using a validation library.`);
// Decide whether to throw an error or attempt send anyway based on requirements.// Example: throw new BadRequestException('Invalid destination phone number format. Use international format.');
}
const payload = {
messages: [
{
destinations: [{ to }],
text,
// Set sender ID if provided, otherwise Infobip might use a default shared number (depends on account setup)
...(from && { from }),
},
],
};
try {
// Use the Infobip SDK to send the SMSconst response = awaitthis.infobipClient.channels.sms.send({
type: 'text', // Specify message typemessages: payload.messages,
});
this.logger.log(`Infobip SMS API Response: ${JSON.stringify(response.data)}`);
// Optional: Check response status - Infobip often returns 2xx even for partial failures within a bulk send.// Detailed status is usually within response.data.messages[0].statusconst messageStatus = response.data?.messages?.[0]?.status;
// Group ID 5 typically indicates 'REJECTED' status group according to Infobip documentation. Check Infobip docs for definitive status codes.if (messageStatus?.groupId === 5) {
this.logger.error(`SMS to ${to} rejected by Infobip: ${messageStatus.description}`);
// Throw an exception or return a specific failure statusthrownewBadRequestException(`SMS rejected: ${messageStatus.description}`);
}
this.logger.log(`Successfully requested SMS send to ${to}. Message ID: ${response.data?.messages?.[0]?.messageId}`);
return response.data; // Return the successful response data
} catch (error) {
this.logger.error(`Failed to send SMS to ${to}: ${error.message}`, error.stack);
// Handle potential Infobip API errors (often have a response object)if (error.response?.data?.requestError?.serviceException) {
const infobipError = error.response.data.requestError.serviceException;
this.logger.error(`Infobip API Error: ${infobipError.messageId} - ${infobipError.text}`);
thrownewInternalServerErrorException(`Infobip API Error: ${infobipError.text}`);
}
// Handle generic errorsthrownewInternalServerErrorException('Failed to send SMS due to an internal error.');
}
}
}
Error Handling: Includes basic validation, SDK call within try...catch, logging success/errors, and parsing potential Infobip-specific errors from the response.
Sender ID (from): This is often subject to regulations and pre-registration depending on the country. If omitted, Infobip might use a shared number or a pre-configured default for your account.
Phone Number Format: Emphasizes the need for international format (e.g., 447123456789). Real-world apps should use a robust library like libphonenumber-js for validation.
3. Building the API Layer
Expose the SMS sending functionality via a REST API endpoint using the InfobipController.
Create Data Transfer Object (DTO):
Define a class to represent the expected request body, using class-validator decorators for validation. Create the dto folder if it doesn't exist: mkdir src/infobip/dto.
TypeScript
// src/infobip/dto/send-sms.dto.tsimport { IsString, IsNotEmpty, IsOptional, Matches, MaxLength } from'class-validator';
exportclassSendSmsDto {
@IsString()
@IsNotEmpty()
// Basic regex allowing optional '+' and digits.// WARNING: Insufficient for robust validation. Use a library like libphonenumber-js in production.@Matches(/^\+?\d{10,15}$/, { message: 'Phone number must be in international format (e.g., +447123456789). Basic validation only.'})
to: string;
@IsString()
@IsNotEmpty()
// SMS character limits per GSM 03.38 / 3GPP 23.038 specification:// - 160 characters for GSM-7 encoding (standard 7-bit alphabet)// - 70 characters for UCS-2/UTF-16 encoding (Unicode, e.g., emoji, non-Latin scripts)// Messages exceeding these limits are automatically concatenated by carriers.// The API handles concatenation; max 1600 allows ~10 concatenated GSM-7 segments or ~23 Unicode segments.@MaxLength(1600)
text: string;
@IsString()
@IsOptional()
@MaxLength(11) // Alphanumeric sender ID max lengthfrom?: string;
}
Define Controller Endpoint:
Create a POST endpoint that accepts the SendSmsDto and calls the InfobipService.
TypeScript
// src/infobip/infobip.controller.tsimport { Controller, Post, Body, UsePipes, ValidationPipe, HttpCode, HttpStatus } from'@nestjs/common';
import { InfobipService } from'./infobip.service';
import { SendSmsDto } from'./dto/send-sms.dto';
@Controller('infobip') // Route prefix: /infobipexportclassInfobipController {
constructor(privatereadonlyinfobipService: InfobipService) {}
@Post('sms/send') // Full route: POST /infobip/sms/send@HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted on successful request queueing@UsePipes(newValidationPipe({ transform: true, whitelist: true })) // Enable validationasyncsendSms(@Body() sendSmsDto: SendSmsDto) {
const { to, text, from } = sendSmsDto;
// Service method handles actual sending and detailed error handlingconst result = awaitthis.infobipService.sendSms(to, text, from);
return {
message: 'SMS request accepted successfully.',
details: result, // Include Infobip's response details
};
}
}
@UsePipes(new ValidationPipe(...)): Automatically validates the incoming request body against the SendSmsDto.
transform: true: Attempts to transform plain JavaScript object to DTO instance.
whitelist: true: Strips any properties not defined in the DTO.
@HttpCode(HttpStatus.ACCEPTED): Sets the default success status code to 202, indicating the request was accepted for processing, which is suitable for asynchronous operations like sending SMS.
Enable Global Validation Pipe (Recommended):
Instead of applying @UsePipes to every controller method, enable it globally in src/main.ts.
TypeScript
// src/main.tsimport { NestFactory } from'@nestjs/core';
import { AppModule } from'./app.module';
import { ValidationPipe, Logger } from'@nestjs/common'; // Import Loggerimport { ConfigService } from'@nestjs/config'; // Import ConfigServiceasyncfunctionbootstrap() {
const app = awaitNestFactory.create(AppModule);
const configService = app.get(ConfigService); // Get ConfigService instanceconst port = configService.get<number>('PORT', 3000); // Get port from env or defaultconst logger = newLogger('Bootstrap'); // Create logger instance// Enable CORS if needed (adjust origin for production)
app.enableCors();
// Global Validation Pipe
app.useGlobalPipes(newValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true, // Optional: Throw error if non-whitelisted properties are present
}));
await app.listen(port);
logger.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
(Remove the @UsePipes decorator from the controller method if you enable it globally).
Testing the Endpoint:
Run the application: npm run start:dev
Use curl or a tool like Postman:
Curl Example:
Bash
curl -X POST http://localhost:3000/infobip/sms/send \
-H "Content-Type: application/json" \
-d '{
"to": "+15551234567",
"text": "Hello from NestJS and Infobip!",
"from": "MyApp"
}'
(Replace +15551234567 with your registered test number if using a trial account. Replace MyApp with your alphanumeric sender ID if configured and allowed).
Example JSON Request:
JSON
{"to":"+15551234567","text":"Test Message via API","from":"TestSender"}
Example JSON Success Response (202 Accepted):
JSON
{"message":"SMS request accepted successfully.","details":{"bulkId":"some-bulk-id-from-infobip","messages":[{"to":"+15551234567","status":{"groupId":1,"groupName":"PENDING","id":7,"name":"PENDING_ENROUTE","description":"Message sent to next instance"},"messageId":"some-message-id-from-infobip"}]}}
Example JSON Validation Error Response (400 Bad Request):
JSON
{"statusCode":400,"message":["Phone number must be in international format (e.g., +447123456789). Basic validation only.","text should not be empty"],"error":"Bad Request"}
4. Integrating with Third-Party Services (Infobip Details)
We've already integrated the SDK, but let's reiterate the crucial configuration steps.
API Key: Navigate to the API Keys section (often accessible from the homepage or account settings). Generate a new API key if you don't have one. Copy the key securely.
Base URL: Your unique Base URL is typically displayed prominently on the Infobip Portal homepage after logging in (e.g., xxxxx.api.infobip.com). Copy this URL.
Configure Environment Variables:
As done in Step 1.3, place INFOBIP_API_KEY and INFOBIP_BASE_URL in your .env file.
Purpose:
INFOBIP_API_KEY: Authenticates your application with the Infobip API. Treat it like a password.
INFOBIP_BASE_URL: Tells the SDK which regional Infobip endpoint to communicate with.
Format: These are typically strings provided directly by Infobip.
Secure Handling:
Never commit API keys or sensitive credentials directly into your code or version control (Git).
Use the .env file and ensure it's listed in .gitignore.
In production environments, manage secrets using secure methods provided by your cloud provider (e.g., AWS Secrets Manager, Google Secret Manager, Azure Key Vault) or environment variable injection in your deployment platform.
Fallback Mechanisms (Conceptual):
While full implementation is complex, consider these for production:
Retries: Implement retries with exponential backoff (covered briefly in the next section) for transient network errors or temporary Infobip API unavailability (e.g., 5xx errors).
Queuing: For high-volume sending or increased resilience, place SMS requests into a message queue (like RabbitMQ or AWS SQS). A separate worker process can then consume from the queue and attempt to send via Infobip, handling retries independently.
Monitoring: Monitor Infobip's status page and your application's error rates to detect outages quickly.
5. Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are essential for production.
Consistent Error Strategy:
We've used NestJS's built-in HttpException and its derivatives (InternalServerErrorException, BadRequestException). This provides standardized HTTP error responses.
The ValidationPipe handles input validation errors automatically, returning 400 Bad Request.
The InfobipService catches errors during SDK interaction, logs details, and throws appropriate HttpExceptions.
Logging:
NestJS's built-in Logger is used.
Levels: Use appropriate levels (log for general info, warn for potential issues, error for failures).
Format: The default logger provides timestamps, context (class name), and messages. For production, consider structured logging libraries (like pino with nestjs-pino) to output JSON logs, which are easier for log aggregation tools (Datadog, Splunk, ELK stack) to parse.
What to Log: Log key events (incoming requests, SMS send attempts), errors (including stack traces and Infobip API error details), and successful outcomes (like the returned messageId).
Retry Strategy (Conceptual):
When to Retry: Network issues, rate limiting errors (e.g., HTTP 429), transient server errors from Infobip (HTTP 5xx).
When NOT to Retry: Validation errors (400), authentication errors (401), permanent rejections (like invalid number formats if Infobip returns a specific error code), configuration errors.
Implementation: Use a library like async-retry or p-retry.
Example Idea (using async-retry - requires npm i async-retry):
TypeScript
// Inside InfobipService - conceptual example, adapt as neededimport * as retry from'async-retry';
asyncsendSmsWithRetry(to: string, text: string, from?: string) {
returnretry(async (bail, attempt) => {
this.logger.log(`Attempt ${attempt} to send SMS to ${to}`);
try {
// Original sendSms logic using this.infobipClient.channels.sms.send(...)const response = awaitthis.infobipClient.channels.sms.send({
type: 'text',
messages: [{ destinations: [{ to }], text, ...(from && { from }) }]
});
// Check for specific reject statuses if neededconst messageStatus = response.data?.messages?.[0]?.status;
// Group ID 5 typically indicates 'REJECTED' status group according to Infobip documentation. Check Infobip docs for definitive status codes.if (messageStatus?.groupId === 5) {
this.logger.error(`SMS rejected permanently, not retrying: ${messageStatus.description}`);
// Use bail() to stop retrying for non-transient errorsbail(newError(`SMS Rejected: ${messageStatus.description}`));
return; // bail throws, so this won't be reached but satisfies TS
}
return response.data;
} catch (error) {
this.logger.warn(`Attempt ${attempt} failed: ${error.message}`);
// Check if the error is something we shouldn't retry (e.g., 4xx client errors other than 429)if (error.response?.status >= 400 && error.response?.status !== 429 && error.response?.status < 500) {
this.logger.error(`Non-retryable client error (${error.response.status}), stopping retries.`);
bail(error); // Stop retryingreturn;
}
// For other errors (network, 5xx, 429), throw to trigger retrythrow error;
}
}, {
retries: 3, // Number of retriesfactor: 2, // Exponential backoff factorminTimeout: 1000, // Minimum wait time (ms)onRetry: (error, attempt) => {
this.logger.warn(`Retrying SMS send (attempt ${attempt}) due to error: ${error.message}`);
}
});
}
Testing Error Scenarios:
Send requests with invalid data (missing fields, bad phone format) to test ValidationPipe.
Temporarily modify the API key/Base URL in .env to test authentication/configuration errors.
Mock the infobipClient.channels.sms.send method in unit tests (using Jest mocks) to throw specific errors and verify your service handles them correctly.
6. Creating a Database Schema and Data Layer (Optional Logging)
Storing logs of sent messages can be useful for tracking and auditing. We'll use TypeORM and PostgreSQL as an example.
Install Dependencies:
Bash
# Using npm
npm install @nestjs/typeorm typeorm pg
# Using yarn
yarn add @nestjs/typeorm typeorm pg
@nestjs/typeorm: NestJS integration for TypeORM.
typeorm: The ORM itself.
pg: PostgreSQL driver.
Configure Database Connection:
Add DB credentials to your .env file (as shown in Step 1.3). Then, configure TypeOrmModule in src/app.module.ts.
// src/infobip/infobip.service.tsimport { Injectable, Logger, InternalServerErrorException, BadRequestException } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import { Infobip, AuthType } from'@infobip-api/sdk';
import { InjectRepository } from'@nestjs/typeorm'; // Import InjectRepositoryimport { Repository } from'typeorm'; // Import Repositoryimport { SmsLog } from'./entities/sms-log.entity'; // Import entity@Injectable()
exportclassInfobipService {
privatereadonly logger = newLogger(InfobipService.name);
privateinfobipClient: Infobip;
constructor(privateconfigService: ConfigService,
@InjectRepository(SmsLog) // Inject the repositoryprivatesmsLogRepository: Repository<SmsLog>,
) {
// ... (Infobip client initialization)const apiKey = this.configService.get<string>('INFOBIP_API_KEY');
const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL');
if (!apiKey || !baseUrl) {
this.logger.error('Infobip API Key or Base URL not configured in environment variables.');
thrownewInternalServerErrorException('Infobip configuration missing.');
}
this.infobipClient = newInfobip({
baseUrl: baseUrl,
apiKey: apiKey,
authType: AuthType.ApiKey,
});
this.logger.log('Infobip client initialized successfully.');
}
asyncsendSms(to: string, text: string, from?: string) {
// ... (validation and setup payload logic from previous sendSms method) ...this.logger.log(`Attempting to send SMS to: ${to}`);
if (!to || !text) {
thrownewBadRequestException('Destination number (to) and text message are required.');
}
// Basic phone number format check (add warning/library recommendation as before)if (!/^\d{10,15}$/.test(to.replace(/^\+/, ''))) {
this.logger.warn(`Potentially invalid phone number format detected by basic check for 'to': ${to}. Consider using a validation library.`);
}
const payload = {
messages: [{ destinations: [{ to }], text, ...(from && { from }) }],
};
let infobipResponseData;
let errorOccurred = false;
leterrorMessage: string | null = null;
try {
const response = awaitthis.infobipClient.channels.sms.send({
type: 'text',
messages: payload.messages,
});
infobipResponseData = response.data;
this.logger.log(`Infobip SMS API Response: ${JSON.stringify(infobipResponseData)}`);
// Optional: Check response status (e.g., groupId 5 for rejection)const messageStatus = infobipResponseData?.messages?.[0]?.status;
if (messageStatus?.groupId === 5) {
this.logger.error(`SMS to ${to} rejected by Infobip: ${messageStatus.description}`);
// Set error state for logging, but throw specific exception
errorOccurred = true;
errorMessage = `SMS rejected: ${messageStatus.description}`;
thrownewBadRequestException(errorMessage);
}
this.logger.log(`Successfully requested SMS send to ${to}.`);
} catch (error) {
errorOccurred = true;
// Use existing error message if already set (e.g., from rejection check)
errorMessage = errorMessage || error.message;
this.logger.error(`Failed to send SMS to ${to}: ${errorMessage}`, error.stack);
// Re-throw the appropriate HttpException after loggingif (error instanceofBadRequestException) { // If it was our rejection errorthrow error;
} elseif (error.response?.data?.requestError?.serviceException) {
const infobipError = error.response.data.requestError.serviceException;
errorMessage = `Infobip API Error: ${infobipError.messageId} - ${infobipError.text}`;
thrownewInternalServerErrorException(errorMessage);
}
// Throw generic error if not handled abovethrownewInternalServerErrorException(errorMessage || 'Failed to send SMS due to an internal error.');
} finally {
// Log the attempt regardless of success or failuretry {
const logEntry = this.smsLogRepository.create({
recipient: to,
sender: from,
messageText: text,
bulkId: infobipResponseData?.bulkId,
messageId: infobipResponseData?.messages?.[0]?.messageId,
statusName: errorOccurred ? 'FAILED_TO_SEND' : (infobipResponseData?.messages?.[0]?.status?.name || 'UNKNOWN'),
statusDescription: errorOccurred ? errorMessage : (infobipResponseData?.messages?.[0]?.status?.description),
statusGroupId: errorOccurred ? -1 : (infobipResponseData?.messages?.[0]?.status?.groupId), // Use -1 or specific code for app-level failure
});
awaitthis.smsLogRepository.save(logEntry);
this.logger.log(`Logged SMS attempt for ${to} with status ${logEntry.statusName}`);
} catch (logError) {
this.logger.error(`Failed to save SMS log for ${to}: ${logError.message}`, logError.stack);
// Consider what to do if logging fails (e.g., just log the logging error, don't fail the main request)
}
}
// Return the successful response data if no error occurred before finally blockif (!errorOccurred) {
return infobipResponseData;
}
// Note: If an error occurred, the exception would have been thrown before this point.
}
}
Frequently Asked Questions (FAQ)
What is the Infobip Node.js SDK and why use it?
The @infobip-api/sdk is the official Infobip library for Node.js that simplifies API authentication and provides type-safe methods for sending SMS, WhatsApp, and email messages. It requires Node.js v14+ and handles API request formatting, authentication headers, and error responses automatically.
How do I validate phone numbers in E.164 format?
E.164 is the ITU-T international standard for phone numbers, specifying a maximum of 15 digits starting with a + sign, followed by the country code (1-3 digits) and subscriber number. For production validation, use the libphonenumber-js library which provides parsePhoneNumber(), isPossible(), and isValid() methods with comprehensive country-specific rules.
What are SMS character limits for GSM-7 vs Unicode encoding?
According to GSM 03.38 (3GPP 23.038) specification:
GSM-7 encoding: 160 characters per message segment (standard 7-bit alphabet for English and Western European languages)
UCS-2/UTF-16 encoding: 70 characters per message segment (for Unicode characters including emoji, Arabic, Chinese, etc.)
Messages exceeding these limits are automatically split into concatenated segments with slight overhead.
This guide reflects current best practices as of January 2025. Always refer to official documentation for the latest API changes and recommendations.
Frequently asked questions
Create a NestJS service that uses the Infobip Node.js SDK. This service will interact with the Infobip API to send SMS messages. You'll need an Infobip account and API key for authentication. The service should handle sending the SMS and any necessary error conditions, such as invalid numbers or network issues. Expose the service's send functionality through a NestJS controller using an appropriate DTO and API endpoint.
The Infobip Node.js SDK (@infobip-api/sdk) is a library that simplifies interaction with Infobip's communication platform APIs. It handles authentication and provides methods to send SMS messages. Using the SDK makes it easier to integrate SMS sending capability into your Node.js and NestJS applications. The setup usually involves initializing an Infobip client instance with your API key and base URL.
NestJS provides structure, modularity, and dependency injection. Its modular architecture organizes the project, making it easier to maintain and test SMS logic in its own module. Dependency injection simplifies testing and swapping implementations.
Consider a message queue like RabbitMQ or AWS SQS for high-volume SMS or increased resilience. This decouples request handling from sending, allowing your application to accept requests quickly and handle failures/retries separately. A worker process can consume messages from the queue and send them via the Infobip API. A queue is ideal for handling occasional network disruptions or delays by providing retry mechanisms for better reliability and prevents slowing down your main application under load.
Yes, you can often use a custom alphanumeric sender ID (up to 11 characters), but this depends on regulations and pre-registration requirements. The from parameter in the SMS sending method allows setting the sender ID. If not provided, Infobip might use a shared number or a default configured for your account. Note that sender ID regulations vary significantly by country, so check Infobip's documentation for specific rules related to the countries you are targeting. A trial account is unlikely to allow arbitrary sender IDs.
Use a try-catch block around the Infobip SDK calls to handle potential errors. The Infobip API often returns a structured error object in its responses, especially in case of network errors or request issues. Use NestJS's built-in HttpException class and its subclasses (BadRequestException, InternalServerErrorException, etc.) to return appropriate error codes to the client. Log details about the error, including stack traces and any Infobip-specific error codes, to help in debugging. Use a structured logger like Pino for more detailed error logging if required. A robust service must handle rate limiting (429 errors), authentication issues (401 errors) and internal Infobip errors (5xx errors), as well as invalid user input.
You need an Infobip account (free or paid), Node.js v14 or higher, npm or yarn, the NestJS CLI, basic understanding of TypeScript, Node.js, and REST APIs, access to a terminal, and optionally Git and Docker.
Use your package manager (npm or yarn): npm install @infobip-api/sdk @nestjs/config class-validator class-transformer or yarn add @infobip-api/sdk @nestjs/config class-validator class-transformer. These install the Infobip SDK, configuration, and validation libraries.
You need INFOBIP_API_KEY and INFOBIP_BASE_URL from your Infobip account dashboard. Store them securely in a .env file and load them using @nestjs/config. Never commit .env to Git.
Use the NestJS CLI: nest new infobip-sms-service. Then, navigate to the created project directory: cd infobip-sms-service.
Create an Infobip module, service, and controller: nest generate module infobip, nest generate service infobip, and nest generate controller infobip. The module encapsulates related components. The service handles the Infobip logic, and the controller exposes the API endpoint.
Create a Data Transfer Object (DTO) with class-validator decorators. Use the ValidationPipe in the controller or globally to automatically validate requests. Validate to, text, and optional from fields, and implement a basic check on the number format. Consider using an external library like libphonenumber-js for production-ready validation.
Use a POST request to an endpoint like /infobip/sms/send. The request body should contain the to (recipient), text (message), and optionally from (sender ID) fields in JSON format.
Use NestJS's built-in Logger or integrate a dedicated logging library for structured JSON logging. Log key events like successful sends, failures, and API responses. Include relevant data (message ID, recipient, status) for easier debugging and tracking. For production, consider structured logging with a logging library (like pino or winston) and log aggregation tools (Datadog, Splunk).
Use an exponential backoff retry mechanism with a library like async-retry or p-retry. Retry on transient errors like network issues or 5xx errors from Infobip. Don't retry on validation errors or permanent failures. Be sure to log retry attempts and stop retrying after a reasonable number of attempts. Be mindful of idempotency requirements if retries are implemented.
This comprehensive guide walks you through building a production-ready SMS service using Node.js, the NestJS framework, and the Infobip API. You'll learn how to implement secure API authentication, robust phone number validation using E.164 standards, comprehensive error handling, and deployment strategies for reliable SMS delivery—essential for marketing campaigns, transactional messaging, and two-factor authentication systems.
We'll use the official Infobip Node.js SDK (
@infobip-api/sdk) for seamless integration. By the end, you'll have a fully deployable NestJS module with logging, validation, configuration management, and best practices for production environments.Project Overview: Building a Scalable SMS Microservice
This guide provides a complete walkthrough for building a robust service using Node.js and the NestJS framework to send SMS messages via the Infobip API. We'll cover everything from project setup and core implementation to error handling, security, deployment, and monitoring, enabling you to integrate reliable SMS functionality into your applications, often a key component of marketing campaigns.
We'll focus on using the official Infobip Node.js SDK for seamless integration. By the end, you'll have a deployable NestJS module capable of sending SMS messages, complete with logging, validation, and configuration management.
Project Overview and Goals
What We'll Build:
A NestJS microservice or module with a dedicated API endpoint (
/infobip/sms/send) that accepts requests to send SMS messages. This service will securely interact with the Infobip API using their official Node.js SDK.Problem Solved:
Provides a centralized, scalable, and maintainable way to handle SMS sending logic within a larger application ecosystem. Decouples SMS functionality from other business logic, making the system easier to manage and test. Enables programmatic sending of SMS for notifications, alerts, 2FA, or as part of marketing campaign execution flows.
Technologies Used:
@infobip-api/sdk): The official library for interacting with Infobip's communication platform APIs. Simplifies authentication and API calls.System Architecture:
Diagram Placeholder: Client -> NestJS App -> Infobip Service -> Infobip API, with optional logging to a Database
Prerequisites:
@infobip-api/sdkas of January 2025).npm install -g @nestjs/cli).Expected Outcome:
A functional NestJS application with an endpoint to send SMS messages via Infobip, incorporating best practices for configuration, error handling, validation, and logging.
1. Setting up the Project
Let's initialize our NestJS project and install the necessary dependencies.
Create NestJS Project: Open your terminal and run the NestJS CLI command:
nest new infobip-sms-service cd infobip-sms-serviceChoose your preferred package manager (npm or yarn) when prompted.
Install Dependencies: We need the Infobip SDK, a configuration module, and validation libraries.
# Using npm npm install @infobip-api/sdk @nestjs/config class-validator class-transformer # Using yarn yarn add @infobip-api/sdk @nestjs/config class-validator class-transformer@infobip-api/sdk: The official Infobip SDK.@nestjs/config: For managing environment variables securely.class-validator&class-transformer: For validating incoming request data (DTOs).Configure Environment Variables: NestJS promotes using environment variables for configuration. Create a
.envfile in the project root:INFOBIP_API_KEY: Obtain this from your Infobip account dashboard (usually under API Keys).INFOBIP_BASE_URL: Find this on your Infobip account dashboard (it's specific to your account, usually displayed prominently on the main dashboard page after login, e.g.,xxxxx.api.infobip.com)..envto your.gitignorefile to prevent committing sensitive credentials.Load Environment Variables: Modify
src/app.module.tsto load the.envfile using@nestjs/config.// src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { InfobipModule } from './infobip/infobip.module'; // We will create this next @Module({ imports: [ ConfigModule.forRoot({ // Load .env file isGlobal: true, // Make ConfigModule available globally }), InfobipModule, // Import our Infobip module ], controllers: [AppController], providers: [AppService], }) export class AppModule {}Create the Infobip Module: Organize Infobip-related logic into its own module.
nest generate module infobip nest generate service infobip --no-spec # Optional: --no-spec skips test file nest generate controller infobip --no-specThis creates
src/infobip/infobip.module.ts,src/infobip/infobip.service.ts, andsrc/infobip/infobip.controller.ts. EnsureInfobipModuleis imported inAppModuleas shown above.2. Implementing Core Functionality (Infobip Service)
The
InfobipServicewill encapsulate the logic for interacting with the Infobip SDK.Initialize Infobip Client: Inject
ConfigServiceto access environment variables and initialize the Infobip client instance.// src/infobip/infobip.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; // Import Infobip SDK elements @Injectable() export class InfobipService { private readonly logger = new Logger(InfobipService.name); private infobipClient: Infobip; constructor(private configService: ConfigService) { const apiKey = this.configService.get<string>('INFOBIP_API_KEY'); const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL'); if (!apiKey || !baseUrl) { this.logger.error('Infobip API Key or Base URL not configured in environment variables.'); throw new InternalServerErrorException('Infobip configuration missing.'); } // Initialize the Infobip client this.infobipClient = new Infobip({ baseUrl: baseUrl, apiKey: apiKey, authType: AuthType.ApiKey, // Specify API Key authentication }); this.logger.log('Infobip client initialized successfully.'); } // Method to send SMS will go here }Implement SMS Sending Logic: Create a method to handle sending SMS messages. This method takes the destination number, message text, and optionally the sender ID.
// src/infobip/infobip.service.ts // ... (imports and constructor from above) ... export class InfobipService { // ... (logger, infobipClient, constructor) ... async sendSms(to: string, text: string, from?: string) { this.logger.log(`Attempting to send SMS to: ${to}`); // Basic validation (more robust validation in DTO later) if (!to || !text) { throw new BadRequestException('Destination number (to) and text message are required.'); } // WARNING: Basic regex check for phone number format. // This check is insufficient for reliable international format validation (e.g., doesn't enforce '+') // and may allow invalid numbers. Production applications should use a dedicated library like libphonenumber-js // for robust parsing and validation. if (!/^\d{10,15}$/.test(to.replace(/^\+/, ''))) { // Allow optional '+' at start but otherwise just check digits this.logger.warn(`Potentially invalid phone number format detected by basic check for 'to': ${to}. Consider using a validation library.`); // Decide whether to throw an error or attempt send anyway based on requirements. // Example: throw new BadRequestException('Invalid destination phone number format. Use international format.'); } const payload = { messages: [ { destinations: [{ to }], text, // Set sender ID if provided, otherwise Infobip might use a default shared number (depends on account setup) ...(from && { from }), }, ], }; try { // Use the Infobip SDK to send the SMS const response = await this.infobipClient.channels.sms.send({ type: 'text', // Specify message type messages: payload.messages, }); this.logger.log(`Infobip SMS API Response: ${JSON.stringify(response.data)}`); // Optional: Check response status - Infobip often returns 2xx even for partial failures within a bulk send. // Detailed status is usually within response.data.messages[0].status const messageStatus = response.data?.messages?.[0]?.status; // Group ID 5 typically indicates 'REJECTED' status group according to Infobip documentation. Check Infobip docs for definitive status codes. if (messageStatus?.groupId === 5) { this.logger.error(`SMS to ${to} rejected by Infobip: ${messageStatus.description}`); // Throw an exception or return a specific failure status throw new BadRequestException(`SMS rejected: ${messageStatus.description}`); } this.logger.log(`Successfully requested SMS send to ${to}. Message ID: ${response.data?.messages?.[0]?.messageId}`); return response.data; // Return the successful response data } catch (error) { this.logger.error(`Failed to send SMS to ${to}: ${error.message}`, error.stack); // Handle potential Infobip API errors (often have a response object) if (error.response?.data?.requestError?.serviceException) { const infobipError = error.response.data.requestError.serviceException; this.logger.error(`Infobip API Error: ${infobipError.messageId} - ${infobipError.text}`); throw new InternalServerErrorException(`Infobip API Error: ${infobipError.text}`); } // Handle generic errors throw new InternalServerErrorException('Failed to send SMS due to an internal error.'); } } }try...catch, logging success/errors, and parsing potential Infobip-specific errors from the response.from): This is often subject to regulations and pre-registration depending on the country. If omitted, Infobip might use a shared number or a pre-configured default for your account.447123456789). Real-world apps should use a robust library likelibphonenumber-jsfor validation.3. Building the API Layer
Expose the SMS sending functionality via a REST API endpoint using the
InfobipController.Create Data Transfer Object (DTO): Define a class to represent the expected request body, using
class-validatordecorators for validation. Create thedtofolder if it doesn't exist:mkdir src/infobip/dto.// src/infobip/dto/send-sms.dto.ts import { IsString, IsNotEmpty, IsOptional, Matches, MaxLength } from 'class-validator'; export class SendSmsDto { @IsString() @IsNotEmpty() // Basic regex allowing optional '+' and digits. // WARNING: Insufficient for robust validation. Use a library like libphonenumber-js in production. @Matches(/^\+?\d{10,15}$/, { message: 'Phone number must be in international format (e.g., +447123456789). Basic validation only.'}) to: string; @IsString() @IsNotEmpty() // SMS character limits per GSM 03.38 / 3GPP 23.038 specification: // - 160 characters for GSM-7 encoding (standard 7-bit alphabet) // - 70 characters for UCS-2/UTF-16 encoding (Unicode, e.g., emoji, non-Latin scripts) // Messages exceeding these limits are automatically concatenated by carriers. // The API handles concatenation; max 1600 allows ~10 concatenated GSM-7 segments or ~23 Unicode segments. @MaxLength(1600) text: string; @IsString() @IsOptional() @MaxLength(11) // Alphanumeric sender ID max length from?: string; }Define Controller Endpoint: Create a POST endpoint that accepts the
SendSmsDtoand calls theInfobipService.// src/infobip/infobip.controller.ts import { Controller, Post, Body, UsePipes, ValidationPipe, HttpCode, HttpStatus } from '@nestjs/common'; import { InfobipService } from './infobip.service'; import { SendSmsDto } from './dto/send-sms.dto'; @Controller('infobip') // Route prefix: /infobip export class InfobipController { constructor(private readonly infobipService: InfobipService) {} @Post('sms/send') // Full route: POST /infobip/sms/send @HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted on successful request queueing @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) // Enable validation async sendSms(@Body() sendSmsDto: SendSmsDto) { const { to, text, from } = sendSmsDto; // Service method handles actual sending and detailed error handling const result = await this.infobipService.sendSms(to, text, from); return { message: 'SMS request accepted successfully.', details: result, // Include Infobip's response details }; } }@UsePipes(new ValidationPipe(...)): Automatically validates the incoming request body against theSendSmsDto.transform: true: Attempts to transform plain JavaScript object to DTO instance.whitelist: true: Strips any properties not defined in the DTO.@HttpCode(HttpStatus.ACCEPTED): Sets the default success status code to 202, indicating the request was accepted for processing, which is suitable for asynchronous operations like sending SMS.Enable Global Validation Pipe (Recommended): Instead of applying
@UsePipesto every controller method, enable it globally insrc/main.ts.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; // Import 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 port = configService.get<number>('PORT', 3000); // Get port from env or default const logger = new Logger('Bootstrap'); // Create logger instance // Enable CORS if needed (adjust origin for production) app.enableCors(); // Global Validation Pipe app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true, // Optional: Throw error if non-whitelisted properties are present })); await app.listen(port); logger.log(`Application is running on: ${await app.getUrl()}`); } bootstrap();(Remove the
@UsePipesdecorator from the controller method if you enable it globally).Testing the Endpoint: Run the application:
npm run start:devUse
curlor a tool like Postman:Curl Example:
curl -X POST http://localhost:3000/infobip/sms/send \ -H "Content-Type: application/json" \ -d '{ "to": "+15551234567", "text": "Hello from NestJS and Infobip!", "from": "MyApp" }'(Replace
+15551234567with your registered test number if using a trial account. ReplaceMyAppwith your alphanumeric sender ID if configured and allowed).Example JSON Request:
{ "to": "+15551234567", "text": "Test Message via API", "from": "TestSender" }Example JSON Success Response (202 Accepted):
{ "message": "SMS request accepted successfully.", "details": { "bulkId": "some-bulk-id-from-infobip", "messages": [ { "to": "+15551234567", "status": { "groupId": 1, "groupName": "PENDING", "id": 7, "name": "PENDING_ENROUTE", "description": "Message sent to next instance" }, "messageId": "some-message-id-from-infobip" } ] } }Example JSON Validation Error Response (400 Bad Request):
{ "statusCode": 400, "message": [ "Phone number must be in international format (e.g., +447123456789). Basic validation only.", "text should not be empty" ], "error": "Bad Request" }4. Integrating with Third-Party Services (Infobip Details)
We've already integrated the SDK, but let's reiterate the crucial configuration steps.
Obtain Credentials:
xxxxx.api.infobip.com). Copy this URL.Configure Environment Variables:
INFOBIP_API_KEYandINFOBIP_BASE_URLin your.envfile.INFOBIP_API_KEY: Authenticates your application with the Infobip API. Treat it like a password.INFOBIP_BASE_URL: Tells the SDK which regional Infobip endpoint to communicate with.Secure Handling:
.envfile and ensure it's listed in.gitignore.Fallback Mechanisms (Conceptual): While full implementation is complex, consider these for production:
5. Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are essential for production.
Consistent Error Strategy:
HttpExceptionand its derivatives (InternalServerErrorException,BadRequestException). This provides standardized HTTP error responses.ValidationPipehandles input validation errors automatically, returning 400 Bad Request.InfobipServicecatches errors during SDK interaction, logs details, and throws appropriateHttpExceptions.Logging:
Loggeris used.logfor general info,warnfor potential issues,errorfor failures).pinowithnestjs-pino) to output JSON logs, which are easier for log aggregation tools (Datadog, Splunk, ELK stack) to parse.messageId).Retry Strategy (Conceptual):
async-retryorp-retry.async-retry- requiresnpm i async-retry):// Inside InfobipService - conceptual example, adapt as needed import * as retry from 'async-retry'; async sendSmsWithRetry(to: string, text: string, from?: string) { return retry(async (bail, attempt) => { this.logger.log(`Attempt ${attempt} to send SMS to ${to}`); try { // Original sendSms logic using this.infobipClient.channels.sms.send(...) const response = await this.infobipClient.channels.sms.send({ type: 'text', messages: [{ destinations: [{ to }], text, ...(from && { from }) }] }); // Check for specific reject statuses if needed const messageStatus = response.data?.messages?.[0]?.status; // Group ID 5 typically indicates 'REJECTED' status group according to Infobip documentation. Check Infobip docs for definitive status codes. if (messageStatus?.groupId === 5) { this.logger.error(`SMS rejected permanently, not retrying: ${messageStatus.description}`); // Use bail() to stop retrying for non-transient errors bail(new Error(`SMS Rejected: ${messageStatus.description}`)); return; // bail throws, so this won't be reached but satisfies TS } return response.data; } catch (error) { this.logger.warn(`Attempt ${attempt} failed: ${error.message}`); // Check if the error is something we shouldn't retry (e.g., 4xx client errors other than 429) if (error.response?.status >= 400 && error.response?.status !== 429 && error.response?.status < 500) { this.logger.error(`Non-retryable client error (${error.response.status}), stopping retries.`); bail(error); // Stop retrying return; } // For other errors (network, 5xx, 429), throw to trigger retry throw error; } }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Minimum wait time (ms) onRetry: (error, attempt) => { this.logger.warn(`Retrying SMS send (attempt ${attempt}) due to error: ${error.message}`); } }); }Testing Error Scenarios:
ValidationPipe..envto test authentication/configuration errors.infobipClient.channels.sms.sendmethod in unit tests (using Jest mocks) to throw specific errors and verify your service handles them correctly.6. Creating a Database Schema and Data Layer (Optional Logging)
Storing logs of sent messages can be useful for tracking and auditing. We'll use TypeORM and PostgreSQL as an example.
Install Dependencies:
# Using npm npm install @nestjs/typeorm typeorm pg # Using yarn yarn add @nestjs/typeorm typeorm pg@nestjs/typeorm: NestJS integration for TypeORM.typeorm: The ORM itself.pg: PostgreSQL driver.Configure Database Connection: Add DB credentials to your
.envfile (as shown in Step 1.3). Then, configureTypeOrmModuleinsrc/app.module.ts.// src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { InfobipModule } from './infobip/infobip.module'; import { TypeOrmModule } from '@nestjs/typeorm'; // Import TypeOrmModule import { SmsLog } from './infobip/entities/sms-log.entity'; // We will create this next @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRootAsync({ // Async config to use ConfigService imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ type: 'postgres', host: configService.get<string>('DB_HOST', 'localhost'), port: configService.get<number>('DB_PORT', 5432), username: configService.get<string>('DB_USERNAME'), password: configService.get<string>('DB_PASSWORD'), database: configService.get<string>('DB_DATABASE'), entities: [SmsLog], // Register our entity synchronize: configService.get<string>('NODE_ENV') !== 'production', // Auto-create schema in dev ONLY. Use migrations in prod. // logging: true, // Enable detailed SQL logging if needed }), }), InfobipModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {}synchronize: true(DEV ONLY): Automatically creates/updates database tables based on entities. Never use this in production. Use migrations instead.Create Entity: Define the structure of the log table. Create the
entitiesfolder:mkdir src/infobip/entities.// src/infobip/entities/sms-log.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm'; @Entity('sms_logs') // Table name export class SmsLog { @PrimaryGeneratedColumn('uuid') id: string; @Index() // Index for faster lookups @Column({ nullable: true }) // Infobip might not return messageId immediately or in all error cases messageId?: string; @Column({ nullable: true }) bulkId?: string; @Column() recipient: string; // The 'to' number @Column({ nullable: true }) sender?: string; // The 'from' sender ID @Column({ type: 'text' }) // Use text for potentially long messages messageText: string; @Index() @Column() // e.g., PENDING_ENROUTE, DELIVERED_TO_HANDSET, REJECTED statusName: string; @Column({ nullable: true }) statusDescription?: string; @Column({ nullable: true }) statusGroupId?: number; // e.g., 1 (PENDING), 3 (DELIVERED), 5 (REJECTED) @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; }Register Entity and Inject Repository: Make the
SmsLogentity available within theInfobipModuleand inject its repository into theInfobipService.// src/infobip/infobip.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; // Import TypeOrmModule import { InfobipService } from './infobip.service'; import { InfobipController } from './infobip.controller'; import { SmsLog } from './entities/sms-log.entity'; // Import the entity @Module({ imports: [ TypeOrmModule.forFeature([SmsLog]), // Make SmsLog repository available ], controllers: [InfobipController], providers: [InfobipService], }) export class InfobipModule {}// src/infobip/infobip.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; import { InjectRepository } from '@nestjs/typeorm'; // Import InjectRepository import { Repository } from 'typeorm'; // Import Repository import { SmsLog } from './entities/sms-log.entity'; // Import entity @Injectable() export class InfobipService { private readonly logger = new Logger(InfobipService.name); private infobipClient: Infobip; constructor( private configService: ConfigService, @InjectRepository(SmsLog) // Inject the repository private smsLogRepository: Repository<SmsLog>, ) { // ... (Infobip client initialization) const apiKey = this.configService.get<string>('INFOBIP_API_KEY'); const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL'); if (!apiKey || !baseUrl) { this.logger.error('Infobip API Key or Base URL not configured in environment variables.'); throw new InternalServerErrorException('Infobip configuration missing.'); } this.infobipClient = new Infobip({ baseUrl: baseUrl, apiKey: apiKey, authType: AuthType.ApiKey, }); this.logger.log('Infobip client initialized successfully.'); } async sendSms(to: string, text: string, from?: string) { // ... (validation and setup payload logic from previous sendSms method) ... this.logger.log(`Attempting to send SMS to: ${to}`); if (!to || !text) { throw new BadRequestException('Destination number (to) and text message are required.'); } // Basic phone number format check (add warning/library recommendation as before) if (!/^\d{10,15}$/.test(to.replace(/^\+/, ''))) { this.logger.warn(`Potentially invalid phone number format detected by basic check for 'to': ${to}. Consider using a validation library.`); } const payload = { messages: [{ destinations: [{ to }], text, ...(from && { from }) }], }; let infobipResponseData; let errorOccurred = false; let errorMessage: string | null = null; try { const response = await this.infobipClient.channels.sms.send({ type: 'text', messages: payload.messages, }); infobipResponseData = response.data; this.logger.log(`Infobip SMS API Response: ${JSON.stringify(infobipResponseData)}`); // Optional: Check response status (e.g., groupId 5 for rejection) const messageStatus = infobipResponseData?.messages?.[0]?.status; if (messageStatus?.groupId === 5) { this.logger.error(`SMS to ${to} rejected by Infobip: ${messageStatus.description}`); // Set error state for logging, but throw specific exception errorOccurred = true; errorMessage = `SMS rejected: ${messageStatus.description}`; throw new BadRequestException(errorMessage); } this.logger.log(`Successfully requested SMS send to ${to}.`); } catch (error) { errorOccurred = true; // Use existing error message if already set (e.g., from rejection check) errorMessage = errorMessage || error.message; this.logger.error(`Failed to send SMS to ${to}: ${errorMessage}`, error.stack); // Re-throw the appropriate HttpException after logging if (error instanceof BadRequestException) { // If it was our rejection error throw error; } else if (error.response?.data?.requestError?.serviceException) { const infobipError = error.response.data.requestError.serviceException; errorMessage = `Infobip API Error: ${infobipError.messageId} - ${infobipError.text}`; throw new InternalServerErrorException(errorMessage); } // Throw generic error if not handled above throw new InternalServerErrorException(errorMessage || 'Failed to send SMS due to an internal error.'); } finally { // Log the attempt regardless of success or failure try { const logEntry = this.smsLogRepository.create({ recipient: to, sender: from, messageText: text, bulkId: infobipResponseData?.bulkId, messageId: infobipResponseData?.messages?.[0]?.messageId, statusName: errorOccurred ? 'FAILED_TO_SEND' : (infobipResponseData?.messages?.[0]?.status?.name || 'UNKNOWN'), statusDescription: errorOccurred ? errorMessage : (infobipResponseData?.messages?.[0]?.status?.description), statusGroupId: errorOccurred ? -1 : (infobipResponseData?.messages?.[0]?.status?.groupId), // Use -1 or specific code for app-level failure }); await this.smsLogRepository.save(logEntry); this.logger.log(`Logged SMS attempt for ${to} with status ${logEntry.statusName}`); } catch (logError) { this.logger.error(`Failed to save SMS log for ${to}: ${logError.message}`, logError.stack); // Consider what to do if logging fails (e.g., just log the logging error, don't fail the main request) } } // Return the successful response data if no error occurred before finally block if (!errorOccurred) { return infobipResponseData; } // Note: If an error occurred, the exception would have been thrown before this point. } }Frequently Asked Questions (FAQ)
What is the Infobip Node.js SDK and why use it?
The
@infobip-api/sdkis the official Infobip library for Node.js that simplifies API authentication and provides type-safe methods for sending SMS, WhatsApp, and email messages. It requires Node.js v14+ and handles API request formatting, authentication headers, and error responses automatically.How do I validate phone numbers in E.164 format?
E.164 is the ITU-T international standard for phone numbers, specifying a maximum of 15 digits starting with a
+sign, followed by the country code (1-3 digits) and subscriber number. For production validation, use thelibphonenumber-jslibrary which providesparsePhoneNumber(),isPossible(), andisValid()methods with comprehensive country-specific rules.What are SMS character limits for GSM-7 vs Unicode encoding?
According to GSM 03.38 (3GPP 23.038) specification:
How do I handle Infobip API errors in production?
Implement comprehensive error handling by:
error.response.data.requestError.serviceException)What is the recommended approach for SMS logging and monitoring?
For production systems, implement:
Can I use this with other Infobip channels like WhatsApp or Email?
Yes, the Infobip Node.js SDK supports multiple channels through
infobip.channels.*methods:infobip.channels.sms.send()for SMSinfobip.channels.whatsapp.send()for WhatsAppinfobip.channels.email.send()for Email The same authentication and error handling patterns apply across all channels.Summary and Next Steps
You've now built a production-ready Infobip SMS service with Node.js and NestJS featuring:
Next steps to enhance your SMS service:
libphonenumber-jsfor production-grade validation@nestjs/throttlerto prevent abuseFor more information, consult the Infobip API documentation and the NestJS documentation.
This guide reflects current best practices as of January 2025. Always refer to official documentation for the latest API changes and recommendations.