code examples
code examples
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.
How to Build an Infobip SMS Service with Node.js and NestJS for Marketing Campaigns
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/sdkas of January 2025). - npm or yarn package manager.
- NestJS CLI installed globally (
npm install -g @nestjs/cli). - 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:
bashnest 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.
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
.envfile in the project root:dotenv# .env # Infobip Credentials INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL # Application Port (Optional) PORT=3000 # Database Credentials (Optional - Add if implementing logging to DB) # DB_HOST=localhost # DB_PORT=5432 # DB_USERNAME=user # DB_PASSWORD=password # DB_DATABASE=infobip_logsINFOBIP_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
.envto your.gitignorefile to prevent committing sensitive credentials.
text# .gitignore (ensure this line exists or add it) .env -
Load Environment Variables: Modify
src/app.module.tsto load the.envfile using@nestjs/config.typescript// 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.
bashnest 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 InfobipService will encapsulate the logic for interacting with the Infobip SDK.
-
Initialize Infobip Client: Inject
ConfigServiceto access environment variables and initialize the Infobip client instance.typescript// 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.
typescript// 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.'); } } }- 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 likelibphonenumber-jsfor validation.
- Error Handling: Includes basic validation, SDK call within
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.typescript// 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.typescript// 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.typescript// 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:
bashcurl -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:
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.
-
Obtain Credentials:
- Log in to your Infobip Portal.
- 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_KEYandINFOBIP_BASE_URLin your.envfile. - 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.
- As done in Step 1.3, place
-
Secure Handling:
- Never commit API keys or sensitive credentials directly into your code or version control (Git).
- Use the
.envfile 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
HttpExceptionand its derivatives (InternalServerErrorException,BadRequestException). This provides standardized HTTP error responses. - The
ValidationPipehandles input validation errors automatically, returning 400 Bad Request. - The
InfobipServicecatches errors during SDK interaction, logs details, and throws appropriateHttpExceptions.
- We've used NestJS's built-in
-
Logging:
- NestJS's built-in
Loggeris used. - Levels: Use appropriate levels (
logfor general info,warnfor potential issues,errorfor failures). - Format: The default logger provides timestamps, context (class name), and messages. For production, consider structured logging libraries (like
pinowithnestjs-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).
- NestJS's built-in
-
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-retryorp-retry. - Example Idea (using
async-retry- requiresnpm i async-retry):
typescript// 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:
- Send requests with invalid data (missing fields, bad phone format) to test
ValidationPipe. - Temporarily modify the API key/Base URL in
.envto test authentication/configuration errors. - Mock the
infobipClient.channels.sms.sendmethod in unit tests (using Jest mocks) to throw specific errors and verify your service handles them correctly.
- Send requests with invalid data (missing fields, bad phone format) to test
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
.envfile (as shown in Step 1.3). Then, configureTypeOrmModuleinsrc/app.module.ts.typescript// 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.typescript// 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.typescript// 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 {}typescript// 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/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.
How do I handle Infobip API errors in production?
Implement comprehensive error handling by:
- Wrapping SDK calls in try-catch blocks
- Parsing Infobip-specific error responses (check
error.response.data.requestError.serviceException) - Checking message status codes (Group ID 5 indicates REJECTED status)
- Implementing retry logic with exponential backoff for transient errors (5xx, 429 rate limits)
- Logging all errors with context for debugging
What is the recommended approach for SMS logging and monitoring?
For production systems, implement:
- Database logging of all SMS attempts with status tracking (use TypeORM with PostgreSQL)
- Store recipient, sender, message text, Infobip message ID, bulk ID, and status codes
- Index frequently queried fields (messageId, recipient, createdAt)
- Monitor delivery rates and error patterns
- Set up alerts for high failure rates or API connectivity issues
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:
- ✅ Secure API authentication with environment variable management
- ✅ Type-safe TypeScript implementation with NestJS dependency injection
- ✅ Input validation using DTOs and class-validator
- ✅ E.164 phone number format validation guidelines
- ✅ Comprehensive error handling and retry strategies
- ✅ Optional database logging for SMS tracking
- ✅ Production deployment considerations
Next steps to enhance your SMS service:
- Implement phone number validation: Install and integrate
libphonenumber-jsfor production-grade validation - Add rate limiting: Protect your API with
@nestjs/throttlerto prevent abuse - Set up monitoring: Use tools like Datadog, New Relic, or custom metrics to track delivery rates
- Implement queuing: For high-volume scenarios, add RabbitMQ or AWS SQS for reliable message processing
- Add webhook handling: Create endpoints to receive Infobip delivery reports for status updates
- Expand to other channels: Leverage the same patterns for WhatsApp and Email messaging
For 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.
Frequently Asked Questions
How to send SMS with Infobip and NestJS?
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.
What is the Infobip Node.js SDK?
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.
Why use NestJS for an Infobip SMS service?
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.
When should I use a message queue for SMS sending?
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.
Can I use a custom sender ID with Infobip?
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.
How to handle Infobip API errors in NestJS?
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.
What are the prerequisites for setting up this service?
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.
How to install the necessary Infobip dependencies?
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.
What environment variables are required for the Infobip integration?
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.
How to create a NestJS project for the Infobip SMS service?
Use the NestJS CLI: `nest new infobip-sms-service`. Then, navigate to the created project directory: `cd infobip-sms-service`.
How to structure the NestJS project?
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.
How to validate incoming SMS requests?
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.
What should the API endpoint look like?
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.
How to log SMS events effectively?
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).
What is the best way to implement SMS sending retries?
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.