code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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/sdk as 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.

  1. 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.

  2. 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).
  3. Configure Environment Variables: NestJS promotes using environment variables for configuration. Create a .env file 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_logs
    • 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
  4. Load Environment Variables: Modify src/app.module.ts to load the .env file 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 {}
  5. 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.


2. Implementing Core Functionality (Infobip Service)

The InfobipService will encapsulate the logic for interacting with the Infobip SDK.

  1. Initialize Infobip Client: Inject ConfigService to 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
    }
  2. 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 like libphonenumber-js for validation.

3. Building the API Layer

Expose the SMS sending functionality via a REST API endpoint using the InfobipController.

  1. 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.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;
    }
  2. Define Controller Endpoint: Create a POST endpoint that accepts the SendSmsDto and calls the InfobipService.

    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 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.
  3. Enable Global Validation Pipe (Recommended): Instead of applying @UsePipes to every controller method, enable it globally in src/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 @UsePipes decorator from the controller method if you enable it globally).

  4. 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.

  1. 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.
  2. 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).
  3. 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 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}`);
            }
        });
    }
  4. 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.

  1. 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.
  2. Configure Database Connection: Add DB credentials to your .env file (as shown in Step 1.3). Then, configure TypeOrmModule in src/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.
  3. Create Entity: Define the structure of the log table. Create the entities folder: 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;
    }
  4. Register Entity and Inject Repository: Make the SmsLog entity available within the InfobipModule and inject its repository into the InfobipService.

    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:

  1. Wrapping SDK calls in try-catch blocks
  2. Parsing Infobip-specific error responses (check error.response.data.requestError.serviceException)
  3. Checking message status codes (Group ID 5 indicates REJECTED status)
  4. Implementing retry logic with exponential backoff for transient errors (5xx, 429 rate limits)
  5. Logging all errors with context for debugging

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 SMS
  • infobip.channels.whatsapp.send() for WhatsApp
  • infobip.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:

  1. Implement phone number validation: Install and integrate libphonenumber-js for production-grade validation
  2. Add rate limiting: Protect your API with @nestjs/throttler to prevent abuse
  3. Set up monitoring: Use tools like Datadog, New Relic, or custom metrics to track delivery rates
  4. Implement queuing: For high-volume scenarios, add RabbitMQ or AWS SQS for reliable message processing
  5. Add webhook handling: Create endpoints to receive Infobip delivery reports for status updates
  6. 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.