code examples

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

Implementing Two-Way SMS Messaging with Infobip and NestJS

Complete guide to building a production-ready NestJS application for sending and receiving SMS messages using Infobip API, with Prisma database integration and webhook handling.

Implementing Two-Way SMS Messaging with Infobip and NestJS

This guide provides a comprehensive walkthrough for building a production-ready NestJS application capable of both sending outbound SMS messages and receiving inbound SMS messages via Infobip. We will cover project setup, Infobip integration, database interaction using Prisma, webhook handling for incoming messages, error handling, security considerations, and deployment using Docker.

Project Goal: To create a robust NestJS service that:

  1. Sends SMS messages using the Infobip API and Node.js SDK.
  2. Receives incoming SMS messages sent to an Infobip number via webhooks.
  3. Stores a record of both inbound and outbound messages in a PostgreSQL database.
  4. Provides a simple API endpoint to trigger sending messages.
  5. Includes basic security, error handling, and logging.

Technologies Used:

  • Node.js: Runtime environment.
  • NestJS: Progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Chosen for its modular architecture, built-in dependency injection, and TypeScript support.
  • Infobip: Communications Platform as a Service (CPaaS) provider used for sending and receiving SMS messages. We'll use their official Node.js SDK.
  • PostgreSQL: Robust open-source relational database for storing message logs.
  • Prisma: Next-generation ORM for Node.js and TypeScript. Chosen for its type safety, auto-generated migrations, and intuitive API.
  • Docker & Docker Compose: For containerizing the application and its database dependency, ensuring consistent environments.
  • TypeScript: Language used for enhanced type safety and developer productivity.

System Architecture:

text
+-------------+        +---------------------+        +-----------------+        +-------------+
| User / API  |        |   NestJS Backend    |        |   Infobip API   |        | User's Phone|
|   Client    |------->| (Controller,Service)|<------>|  (SMS Sending)  |<------>|             |
+-------------+        |                     |        +-----------------+        +-------------+
                       |  +---------------+  |                                       ^
                       |  | Infobip SDK   |  |                                       | SMS
                       |  +---------------+  |                                       | Reply
                       |                     |                                       |
                       |  +---------------+  |        +------------------+           |
                       |  | Prisma Client |<------>| PostgreSQL DB    |           |
                       |  +---------------+  |        | (Message Logs)   |           v
                       |                     |        +------------------+   +-----------------+
                       |  +---------------+  |                               |   Infobip       |
                       |  | Webhook Handler|<-------------------------------| (Inbound SMS via|
                       |  | (Controller)  |                               |    Webhook)     |
                       |  +---------------+  |                               +-----------------+
                       +---------------------+

Prerequisites:

  • Node.js v16 or higher (v18+ recommended for NestJS 11) and npm or yarn.
  • Docker and Docker Compose installed.
  • An Infobip account (a free trial account works, but has limitations noted later).
  • Access to a PostgreSQL database (we'll run one via Docker).
  • Basic understanding of TypeScript, NestJS concepts, APIs, and databases.
  • A tool for making API requests (like curl or Postman).

1. Setting up the Project

First, we'll initialize a new NestJS project and install necessary dependencies.

1.1. Initialize NestJS Project:

Open your terminal and run the NestJS CLI command to create a new project:

bash
# Ensure you have NestJS CLI installed: npm install -g @nestjs/cli
npx @nestjs/cli new nestjs-infobip-sms
cd nestjs-infobip-sms

This command scaffolds a new project with a standard structure.

1.2. Install Dependencies:

We need several packages for configuration, Infobip integration, database access, and validation.

bash
# Infobip SDK
npm install @infobip-api/sdk

# Prisma ORM
npm install @prisma/client
npm install prisma --save-dev

# NestJS Config Module (for environment variables)
npm install @nestjs/config

# Class Validator & Transformer (for DTO validation)
npm install class-validator class-transformer

# Phone number validation (for proper E.164 validation)
npm install libphonenumber-js

# (Optional, but recommended for webhook security)
npm install @nestjs/throttler # For rate limiting

# (Optional, for real-time updates via WebSockets)
# npm install @nestjs/websockets @nestjs/platform-socket.io

1.3. Initialize Prisma:

Initialize Prisma in your project, specifying PostgreSQL as the database provider.

bash
npx prisma init --datasource-provider postgresql

This command creates:

  • A prisma directory.
  • A schema.prisma file for defining your database schema.
  • A .env file for environment variables (including the default DATABASE_URL).

1.4. Project Structure Overview:

Your initial src directory will look something like this:

text
src/
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts

We will add modules, services, controllers, DTOs, and configuration files as we build the application.


2. Environment Configuration

Securely managing configuration, especially API keys, is critical. We'll use NestJS's ConfigModule and a .env file.

2.1. Configure .env File:

Open the .env file created by Prisma and add your Infobip credentials and other necessary variables. Remember to add .env to your .gitignore file to avoid committing secrets.

dotenv
# .env

# Database
# Replace user, password, host, port, dbname with your actual local/dev settings
# Example for local setup:
DATABASE_URL=""postgresql://user:password@localhost:5432/mydb?schema=public""
# Example for Docker setup will be shown later, pointing to the 'db' service name.

# Infobip Credentials
# Get these from your Infobip account dashboard (API Keys section)
INFOBIP_API_KEY=""YOUR_INFOBIP_API_KEY""
# Find your specific Base URL in the Infobip dashboard (often like xyz.api.infobip.com)
INFOBIP_BASE_URL=""YOUR_INFOBIP_BASE_URL""

# Application Port
PORT=3000

# Webhook Security (A simple shared secret)
# Generate a strong random string for this
INFOBIP_WEBHOOK_SECRET=""YOUR_STRONG_RANDOM_SECRET""

Create a .env.example file mirroring .env but with placeholder values, and commit this example file to your repository.

2.2. Integrate ConfigModule:

Import and configure ConfigModule in your main application module (src/app.module.ts) to make environment variables accessible throughout the application.

typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; // Import ConfigModule
import { AppController } from './app.controller';
import { AppService } from './app.service';
// We will create these modules later
// import { PrismaModule } from './prisma/prisma.module';
// import { InfobipModule } from './infobip/infobip.module';
// import { MessagingModule } from './messaging/messaging.module';
// import { WebhookModule } from './webhook/webhook.module';

@Module({
  imports: [
    ConfigModule.forRoot({ // Configure ConfigModule
      isGlobal: true, // Make config available globally
      envFilePath: '.env', // Specify the env file path
    }),
    // PrismaModule, // Will uncomment later
    // InfobipModule, // Will uncomment later
    // MessagingModule, // Will uncomment later
    // WebhookModule, // Will uncomment later
  ],
  controllers: [AppController], // Keep default controller for now
  providers: [AppService], // Keep default service for now
})
export class AppModule {}

Explanation:

  • ConfigModule.forRoot() loads variables from the .env file.
  • isGlobal: true makes the ConfigService available application-wide without needing to import ConfigModule into every feature module.

3. Creating a Database Schema and Data Layer (Prisma)

We need a database table to log the SMS messages we send and receive.

3.1. Define the Prisma Schema:

Open prisma/schema.prisma and define the Message model.

prisma
// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL") // Reads from .env file
}

// Define the Message model
model Message {
  id          String    @id @default(cuid()) // Unique identifier
  externalId  String?   @unique // Infobip message ID (optional for initial save, unique once set)
  direction   Direction // INBOUND or OUTBOUND
  sender      String    // Sender phone number or identifier
  recipient   String    // Recipient phone number
  text        String    // Message content
  status      String?   // Status from Infobip (e.g., PENDING, DELIVERED, FAILED)
  rawResponse Json?     // Store raw Infobip response/webhook payload if needed
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}

// Enum for message direction
enum Direction {
  INBOUND
  OUTBOUND
}

Explanation:

  • Message model stores relevant details about each SMS.
  • externalId: Stores the unique messageId provided by Infobip. It's optional initially (?) because we might save the outbound message before getting the ID, and nullable because inbound messages might not always have one initially in certain error states. Marked @unique as Infobip IDs should be unique.
  • direction: Uses an enum for clarity.
  • rawResponse: Useful for debugging, stores the full JSON payload from Infobip.

3.2. Run Database Migration:

Apply the schema changes to your database using Prisma Migrate. This command generates SQL and executes it.

bash
# This creates a migration file and applies it to the database
npx prisma migrate dev --name init-message-model

Prisma will prompt you to name the migration (e.g., init-message-model) and then create the necessary SQL migration file in the prisma/migrations directory and apply it to the database specified in your DATABASE_URL. Ensure your PostgreSQL server is running. If running via Docker Compose (shown later), you'll run migrations after the container starts.

3.3. Create Prisma Service:

Create a reusable Prisma service following NestJS best practices.

bash
# Create module and service files
mkdir src/prisma
touch src/prisma/prisma.module.ts
touch src/prisma/prisma.service.ts
typescript
// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    // Connect to the database when the module is initialized
    await this.$connect();
  }

  async onModuleDestroy() {
    // Disconnect from the database when the application shuts down
    await this.$disconnect();
  }
}
typescript
// src/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global() // Make PrismaService available globally
@Module({
  providers: [PrismaService],
  exports: [PrismaService], // Export the service for injection
})
export class PrismaModule {}

Finally, import PrismaModule into AppModule:

typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module'; // Import PrismaModule
// ... other imports

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    PrismaModule, // Add PrismaModule here
    // InfobipModule,
    // MessagingModule,
    // WebhookModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

4. Integrating with Infobip (Sending SMS)

Now, let's set up the Infobip SDK to send SMS messages.

4.1. Create Infobip Module and Service:

We'll encapsulate Infobip logic within its own module.

bash
mkdir src/infobip
touch src/infobip/infobip.module.ts
touch src/infobip/infobip.service.ts

4.2. Implement Infobip Service:

This service will initialize the Infobip client and provide a method for sending SMS.

typescript
// src/infobip/infobip.service.ts
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Infobip, AuthType } from '@infobip-api/sdk';

@Injectable()
export class InfobipService implements OnModuleInit {
  private readonly logger = new Logger(InfobipService.name);
  private infobipClient: Infobip;

  constructor(private configService: ConfigService) {}

  onModuleInit() {
    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. SMS functionality disabled.');
      // You might throw an error here if Infobip is essential for startup
      return;
    }

    try {
      this.infobipClient = new Infobip({
        baseUrl: baseUrl,
        apiKey: apiKey,
        authType: AuthType.ApiKey,
      });
      this.logger.log('Infobip client initialized successfully.');
    } catch (error) {
      this.logger.error('Failed to initialize Infobip client:', error);
    }
  }

  async sendSms(to: string, text: string, from?: string): Promise<any> {
    if (!this.infobipClient) {
        this.logger.error('Infobip client not initialized. Cannot send SMS.');
        throw new Error('Infobip service is not available.');
    }

    // **IMPORTANT: Sender ID (`from`)**
    // If 'from' is not provided, use a default or one from config.
    // - Alphanumeric Sender IDs (e.g., 'InfoSMS', 'MyApp'): Cannot receive replies. Good for alerts/notifications.
    // - Purchased/Verified Numbers: Required for two-way communication (receiving replies). Must be obtained via Infobip.
    // Regulations vary by country. Using 'InfoSMS' as a default might be misleading if replies are expected.
    const sender = from || 'InfoSMS'; // Example default. Configure appropriately.

    // Ensure 'to' number is in international E.164 format (e.g., 447123456789)
    // **WARNING:** The regex below is extremely basic and insufficient for production validation.
    // It does NOT properly validate E.164 (missing '+', country code checks, etc.).
    // Strongly consider using a robust library like 'libphonenumber-js' for reliable validation.
    if (!/^\d{11,15}$/.test(to.replace('+', ''))) {
         this.logger.warn(`Recipient phone number format validation is basic and might be inaccurate: ${to}. Consider using a dedicated library. Attempting to send anyway.`);
         // For stricter validation, you might throw an error here:
         // throw new Error(`Invalid recipient phone number format: ${to}`);
    }

    this.logger.log(`Attempting to send SMS to ${to} from ${sender}`);

    try {
      const response = await this.infobipClient.channels.sms.send({
        messages: [
          {
            destinations: [{ to: to }],
            from: sender,
            text: text,
          },
        ],
      });

      this.logger.log(`SMS sent successfully via Infobip. Bulk ID: ${response.data.bulkId}`);
      // Consider logging specific message IDs if needed: response.data.messages[0]?.messageId
      return response.data; // Return the response data from Infobip
    } catch (error) {
      this.logger.error('Failed to send SMS via Infobip:', error.response?.data || error.message);
      // Rethrow or handle the error appropriately
      throw error;
    }
  }
}

Explanation:

  • Injects ConfigService to retrieve API Key and Base URL.
  • Initializes the Infobip client in onModuleInit. Includes basic error handling for configuration issues.
  • sendSms method constructs the payload and uses infobipClient.channels.sms.send.
  • Includes logging for better traceability.
  • Includes a warning about the basic phone number validation and recommends a library.
  • Manages the from address (Sender ID) with strong emphasis that alphanumeric IDs cannot receive replies and a purchased/verified number must be used for two-way communication.
  • Returns the Infobip API response on success, throws an error on failure.

4.3. Implement Infobip Module:

typescript
// src/infobip/infobip.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; // ConfigModule is needed here if not global
import { InfobipService } from './infobip.service';

@Module({
  imports: [ConfigModule], // Ensure ConfigService is available
  providers: [InfobipService],
  exports: [InfobipService], // Export the service
})
export class InfobipModule {}

4.4. Import Infobip Module:

Add InfobipModule to the imports array in src/app.module.ts.

typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { InfobipModule } from './infobip/infobip.module'; // Import
// ... other imports

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    PrismaModule,
    InfobipModule, // Add InfobipModule
    // MessagingModule,
    // WebhookModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

5. Implementing Core Functionality (Messaging Service)

Create a central service to orchestrate sending messages and saving records to the database.

5.1. Create Messaging Module and Service:

bash
mkdir src/messaging
touch src/messaging/messaging.module.ts
touch src/messaging/messaging.service.ts
touch src/messaging/dto/send-sms.dto.ts # DTO for sending SMS
touch src/messaging/dto/infobip-incoming.dto.ts # DTO for incoming webhook

5.2. Define Data Transfer Objects (DTOs):

typescript
// src/messaging/dto/send-sms.dto.ts
import { IsNotEmpty, IsString, IsPhoneNumber, Length } from 'class-validator';

export class SendSmsDto {
  @IsNotEmpty()
  @IsPhoneNumber(null) // Use null for generic international format check
  @Length(10, 15) // Adjust length constraints as needed
  readonly to: string;

  @IsNotEmpty()
  @IsString()
  @Length(1, 160) // Standard SMS length limit (or more for concatenated)
  readonly text: string;

  // Optional: Allow specifying sender ID if needed and configured
  // @IsOptional()
  // @IsString()
  // readonly from?: string;
}
typescript
// src/messaging/dto/infobip-incoming.dto.ts
// Based on common Infobip webhook structure for SMS MO (Mobile Originated)
// **IMPORTANT:** This DTO is an *example*. You MUST inspect the actual JSON payload
// sent by Infobip to your webhook endpoint (e.g., using webhook.site or logging
// the raw request body) and adjust this structure accordingly. Infobip's payload
// structure can vary based on account configuration and message type.
// Refer to Infobip documentation for the exact 'SMS Message Received' structure.
import { Type } from 'class-transformer';
import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator';

class MessageResult {
  @IsNotEmpty()
  @IsString()
  messageId: string;

  @IsNotEmpty()
  @IsString()
  from: string; // Sender's number

  @IsNotEmpty()
  @IsString()
  to: string; // Your Infobip number

  @IsNotEmpty()
  @IsString()
  text: string; // Message content

  @IsOptional()
  @IsString()
  keyword?: string;

  @IsOptional()
  @IsDateString()
  receivedAt?: string; // Or use @Type(() => Date) if transforming

  @IsOptional()
  @IsNumber()
  smsCount?: number;

  // Add other potential fields based on Infobip docs (price, networkCode etc.)
}

class IncomingSmsPayload {
    // This nested structure is common but verify it!
    @IsNotEmpty()
    @IsArray()
    @ValidateNested({ each: true })
    @Type(() => MessageResult)
    results: MessageResult[];

    @IsOptional()
    @IsNumber()
    messageCount?: number;

    @IsOptional()
    @IsNumber()
    pendingMessageCount?: number;
}

// The top-level DTO for the webhook payload
export class InfobipIncomingSmsDto {
    // Assuming the payload looks like { "results": [...] }
    // **VERIFY THIS STRUCTURE AGAINST ACTUAL INFOBIP PAYLOADS**
    @IsNotEmpty()
    @IsArray()
    @ValidateNested({ each: true })
    @Type(() => MessageResult)
    results: MessageResult[];
}

// --- ALTERNATIVE If payload is directly the array ---
// export class InfobipIncomingSmsDto extends Array<MessageResult> {}
// You would need to adjust validation pipes for arrays if using this.

5.3. Implement Messaging Service:

typescript
// src/messaging/messaging.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { InfobipService } from '../infobip/infobip.service';
import { SendSmsDto } from './dto/send-sms.dto';
import { InfobipIncomingSmsDto, MessageResult } from './dto/infobip-incoming.dto';
import { Direction } from '@prisma/client'; // Import enum from generated client

@Injectable()
export class MessagingService {
  private readonly logger = new Logger(MessagingService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly infobip: InfobipService,
  ) {}

  async handleOutgoingSms(sendSmsDto: SendSmsDto): Promise<{ messageId: string | null; dbId: string }> {
    this.logger.log(`Handling outgoing SMS request to ${sendSmsDto.to}`);

    let dbMessage;
    try {
        // 1. Save initial record to DB (optional, could save after sending)
        dbMessage = await this.prisma.message.create({
            data: {
                direction: Direction.OUTBOUND,
                recipient: sendSmsDto.to,
                sender: 'SYSTEM', // Or the specific sender ID used
                text: sendSmsDto.text,
                status: 'PENDING_SEND', // Initial status
            },
        });
        this.logger.log(`Saved initial outgoing message record with ID: ${dbMessage.id}`);

        // 2. Send SMS via Infobip
        const infobipResponse = await this.infobip.sendSms(sendSmsDto.to, sendSmsDto.text /*, sendSmsDto.from */);

        // 3. Update DB record with Infobip details
        const messageInfo = infobipResponse.messages?.[0];
        if (messageInfo) {
            await this.prisma.message.update({
                where: { id: dbMessage.id },
                data: {
                    externalId: messageInfo.messageId,
                    status: messageInfo.status?.name || 'SENT_TO_INFOBIP', // Use status from response
                    sender: messageInfo.from || 'SYSTEM', // Update sender if available in response
                    // Using `as any` bypasses type safety. Pragmatic for storing raw external data,
                    // but consider defining a basic interface for `infobipResponse` or using
                    // `unknown` with type guards for better practice if structure is known/stable.
                    rawResponse: infobipResponse as any,
                },
            });
             this.logger.log(`Updated message ${dbMessage.id} with Infobip ID: ${messageInfo.messageId}`);
            return { messageId: messageInfo.messageId, dbId: dbMessage.id };
        } else {
             this.logger.warn(`Infobip response did not contain message details for DB record ${dbMessage.id}`);
             await this.prisma.message.update({ where: {id: dbMessage.id }, data: { status: 'SENT_NO_DETAILS'}});
             return { messageId: null, dbId: dbMessage.id }; // Indicate success but no ID
        }

    } catch (error) {
        this.logger.error(`Error handling outgoing SMS to ${sendSmsDto.to}:`, error);
        // Update DB record to reflect failure if it was created
        if (dbMessage) {
            await this.prisma.message.update({
                where: { id: dbMessage.id },
                data: { status: 'FAILED_TO_SEND' },
            }).catch(updateError => this.logger.error(`Failed to update message status after error: ${updateError}`));
        }
        // Rethrow the error to be handled by the controller/exception filter
        throw error;
    }
  }

  async handleIncomingSms(payload: InfobipIncomingSmsDto): Promise<void> {
    this.logger.log(`Handling incoming SMS payload with ${payload.results?.length || 0} message(s)`);

    if (!payload || !payload.results || payload.results.length === 0) {
      this.logger.warn('Received empty or invalid incoming SMS payload.');
      return;
    }

    for (const message of payload.results) {
      try {
        const existingMessage = await this.prisma.message.findUnique({
          where: { externalId: message.messageId },
        });

        if (existingMessage) {
          this.logger.warn(`Received duplicate incoming message with Infobip ID: ${message.messageId}. Skipping.`);
          continue; // Avoid processing duplicates
        }

        await this.prisma.message.create({
          data: {
            externalId: message.messageId,
            direction: Direction.INBOUND,
            sender: message.from,
            recipient: message.to,
            text: message.text,
            status: 'RECEIVED', // Or map from a status field in the webhook if available
            // Using `as any` bypasses type safety here too. Consider a basic interface
            // or `unknown` for the `message` object if its structure is reliable.
            rawResponse: message as any, // Store the specific message part
            createdAt: message.receivedAt ? new Date(message.receivedAt) : new Date(), // Use receivedAt if available
          },
        });
        this.logger.log(`Saved incoming message from ${message.from} with Infobip ID: ${message.messageId}`);

        // --- Optional: Trigger further actions ---
        // e.g., Notify other services, process commands in the message text, emit WebSocket event
        // this.eventEmitter.emit('sms.received', savedMessage);

      } catch (error) {
        this.logger.error(`Error processing incoming message ${message.messageId || 'N/A'} from ${message.from}:`, error);
        // Continue processing other messages in the batch
      }
    }
  }
}

Explanation:

  • Injects PrismaService and InfobipService.
  • handleOutgoingSms:
    • Takes the validated SendSmsDto.
    • (Optional) Creates an initial DB record with status PENDING_SEND.
    • Calls InfobipService.sendSms.
    • Updates the DB record with the externalId (Infobip messageId) and status from the response. Includes comment about as any usage.
    • Includes error handling and updates the DB status on failure.
  • handleIncomingSms:
    • Takes the validated InfobipIncomingSmsDto.
    • Iterates through the messages in the payload (Infobip webhooks can batch messages).
    • Checks for duplicates based on externalId (Infobip messageId).
    • Creates a new DB record for each unique incoming message with direction: INBOUND. Includes comment about as any usage.
    • Includes error handling for individual message processing.

5.4. Implement Messaging Module:

typescript
// src/messaging/messaging.module.ts
import { Module } from '@nestjs/common';
import { MessagingService } from './messaging.service';
// Import PrismaModule and InfobipModule if they are not global
// import { PrismaModule } from '../prisma/prisma.module';
// import { InfobipModule } from '../infobip/infobip.module';

@Module({
  // imports: [PrismaModule, InfobipModule], // Needed if not global
  providers: [MessagingService],
  exports: [MessagingService], // Export service for use in Controllers
})
export class MessagingModule {}

5.5. Import Messaging Module:

Add MessagingModule to src/app.module.ts.

typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { InfobipModule } from './infobip/infobip.module';
import { MessagingModule } from './messaging/messaging.module'; // Import
// ... other imports

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    PrismaModule,
    InfobipModule,
    MessagingModule, // Add MessagingModule
    // WebhookModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

6. Building the API Layer (Sending SMS)

Expose an HTTP endpoint to trigger sending SMS messages.

6.1. Create API Controller:

We can modify the default AppController or create a new one (e.g., MessagingController). Let's modify AppController.

typescript
// src/app.controller.ts
import { Controller, Post, Body, HttpCode, HttpStatus, Logger, UsePipes, ValidationPipe } from '@nestjs/common';
// Remove AppService if not used
// import { AppService } from './app.service';
import { MessagingService } from './messaging/messaging.service';
import { SendSmsDto } from './messaging/dto/send-sms.dto';

@Controller('api/v1/messaging') // Define a base path for messaging endpoints
export class AppController {
  private readonly logger = new Logger(AppController.name);

  constructor(private readonly messagingService: MessagingService) {}

  // Remove default GET endpoint if not needed
  // @Get()
  // getHello(): string {
  //   return this.appService.getHello();
  // }

  @Post('sms/send')
  @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as sending is asynchronous
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) // Validate incoming DTO
  async sendSms(@Body() sendSmsDto: SendSmsDto) {
    this.logger.log(`Received request to send SMS to ${sendSmsDto.to}`);
    try {
      const result = await this.messagingService.handleOutgoingSms(sendSmsDto);
      this.logger.log(`SMS queued for sending. DB ID: ${result.dbId}, Infobip ID: ${result.messageId || 'N/A'}`);
      return {
        message: 'SMS submitted successfully.',
        databaseId: result.dbId,
        infobipMessageId: result.messageId, // Can be null if Infobip response was unexpected
      };
    } catch (error) {
      this.logger.error(`API Error sending SMS: ${error.message}`, error.stack);
      // HttpExceptionFilter (added later) will handle formatting the error response
      throw error; // Re-throw for the global filter
    }
  }
}

Explanation:

  • Injects MessagingService.
  • Defines a POST /api/v1/messaging/sms/send endpoint.
  • Uses @UsePipes(new ValidationPipe(...)) to automatically validate the incoming request body against the SendSmsDto.
    • transform: true attempts to convert plain JS objects to DTO instances.
    • whitelist: true strips any properties not defined in the DTO.
  • Calls MessagingService.handleOutgoingSms.
  • Returns a success response with relevant IDs or throws an error which will be caught by our global error handler.
  • Uses HttpStatus.ACCEPTED (202) because the SMS isn't instantly delivered, just accepted for processing.

6.2. Testing the API Endpoint:

Once the application is running, you can test this endpoint using curl:

bash
curl -X POST http://localhost:3000/api/v1/messaging/sms/send \
-H ""Content-Type: application/json"" \
-d '{
  ""to"": ""+12345678900"", # Replace with a valid E.164 number
  ""text"": ""Hello from NestJS and Infobip! Test at '""`date`""'""
}'

# Expected Response (approximate):
# {
#   ""message"": ""SMS submitted successfully."",
#   ""databaseId"": ""cl..."",
#   ""infobipMessageId"": ""2034...""
# }

Remember: If using an Infobip free trial, you can likely only send SMS to the phone number you registered with.


7. Handling Inbound Messages (Webhook)

Infobip notifies your application about incoming SMS messages by sending an HTTP POST request to a predefined URL (webhook).

7.1. Create Web

Frequently Asked Questions

how to send sms with infobip and nestjs

Create a NestJS service that utilizes the Infobip Node.js SDK. This service should handle constructing the SMS message payload, including recipient number, message text, and optional sender ID. Then, use the SDK's 'send' method to dispatch the message through the Infobip API. Ensure your project includes necessary dependencies like '@infobip-api/sdk' and environment variables for your Infobip API key and base URL.

what is infobip sms api

The Infobip SMS API is a cloud-based communication platform service that allows you to send and receive SMS messages programmatically. It provides various features, including sending single and bulk messages, managing contacts, and receiving delivery reports. The API is accessible through different SDKs, including Node.js, making integration into NestJS applications straightforward.

why use nestjs for sms messaging

NestJS provides a robust and structured framework for building server-side applications, making it well-suited for integrating with external APIs like Infobip's SMS API. Its modular architecture, dependency injection, and TypeScript support enhance code organization, testability, and maintainability when handling complex messaging logic. This allows for building scalable and reliable SMS services.

how to integrate infobip with nestjs

Install the Infobip Node.js SDK (`npm install @infobip-api/sdk`), configure the SDK with your API key and base URL from your Infobip account, then create a dedicated NestJS service to encapsulate the Infobip client and sending logic. Ensure to manage your API keys securely in a `.env` file and load them using `@nestjs/config`.

what database is used for message logs

The article recommends PostgreSQL, a robust open-source relational database, for storing SMS message logs. It's used to store details about each message, such as the sender, recipient, message content, and delivery status, ensuring a comprehensive record of communication activities.

how to receive sms with infobip and nestjs

Set up a webhook endpoint in your NestJS application that Infobip can call to deliver incoming SMS messages. Configure this webhook URL in your Infobip account. Use a dedicated controller and DTO to parse the incoming webhook data, validate it, and store it in your database.

what is prisma used for

Prisma is used as an Object-Relational Mapper (ORM) in the article. It simplifies database interactions by providing a type-safe and convenient way to query and manipulate data in the PostgreSQL database, which is where the application's message logs are stored.

when to use alphanumeric sender id with infobip

Use alphanumeric sender IDs (e.g., 'InfoSMS') for one-way communication, like sending alerts or notifications. These IDs cannot receive replies. For two-way messaging, you'll need a purchased or verified phone number through your Infobip account.

can i receive replies with alphanumeric sender id

No, you cannot receive replies to messages sent from an alphanumeric sender ID. Alphanumeric sender IDs are for one-way communication only. To receive replies, you must use a dedicated phone number purchased through Infobip.

how to handle infobip webhooks in nestjs

Create a dedicated controller in your NestJS application with a route that corresponds to the webhook URL you configured in your Infobip account. This controller should parse and validate the incoming webhook payload, which contains information about incoming messages. Store the processed message data in your PostgreSQL database using Prisma.

what is docker used for in this setup

Docker and Docker Compose are used for containerizing the NestJS application and its PostgreSQL database dependency. This simplifies deployment and ensures a consistent environment across different stages of development and production.

when should i save the message to the database

The article suggests saving an initial message record to the database before sending it via Infobip, marked with 'PENDING_SEND' status. After sending, update this record with the Infobip message ID and updated status. Alternatively, save to the database only after a successful send.

how to set up infobip webhook url

Log in to your Infobip account, navigate to the settings for SMS, and locate the webhook configuration section. Add the public URL of your NestJS webhook endpoint, ensuring it's accessible from the internet. This URL is where Infobip will send its POST requests for incoming messages.

what is nestjs configmodule used for

The `@nestjs/config` module is used to manage environment variables securely. It allows you to store sensitive information, like API keys and database credentials, in a `.env` file, which should be excluded from version control, and access these variables within your NestJS application.