code examples
code examples
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:
- Sends SMS messages using the Infobip API and Node.js SDK.
- Receives incoming SMS messages sent to an Infobip number via webhooks.
- Stores a record of both inbound and outbound messages in a PostgreSQL database.
- Provides a simple API endpoint to trigger sending messages.
- 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:
+-------------+ +---------------------+ +-----------------+ +-------------+
| 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
curlor 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:
# Ensure you have NestJS CLI installed: npm install -g @nestjs/cli
npx @nestjs/cli new nestjs-infobip-sms
cd nestjs-infobip-smsThis 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.
# 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.io1.3. Initialize Prisma:
Initialize Prisma in your project, specifying PostgreSQL as the database provider.
npx prisma init --datasource-provider postgresqlThis command creates:
- A
prismadirectory. - A
schema.prismafile for defining your database schema. - A
.envfile for environment variables (including the defaultDATABASE_URL).
1.4. Project Structure Overview:
Your initial src directory will look something like this:
src/
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.tsWe 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.
# .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.
// 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.envfile.isGlobal: truemakes theConfigServiceavailable application-wide without needing to importConfigModuleinto 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/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:
Messagemodel stores relevant details about each SMS.externalId: Stores the uniquemessageIdprovided 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@uniqueas Infobip IDs should be unique.direction: Uses anenumfor 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.
# This creates a migration file and applies it to the database
npx prisma migrate dev --name init-message-modelPrisma 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.
# Create module and service files
mkdir src/prisma
touch src/prisma/prisma.module.ts
touch src/prisma/prisma.service.ts// 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();
}
}// 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:
// 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.
mkdir src/infobip
touch src/infobip/infobip.module.ts
touch src/infobip/infobip.service.ts4.2. Implement Infobip Service:
This service will initialize the Infobip client and provide a method for sending SMS.
// 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
ConfigServiceto retrieve API Key and Base URL. - Initializes the
Infobipclient inonModuleInit. Includes basic error handling for configuration issues. sendSmsmethod constructs the payload and usesinfobipClient.channels.sms.send.- Includes logging for better traceability.
- Includes a warning about the basic phone number validation and recommends a library.
- Manages the
fromaddress (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:
// 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.
// 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:
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 webhook5.2. Define Data Transfer Objects (DTOs):
// 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;
}// 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:
// 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
PrismaServiceandInfobipService. 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(InfobipmessageId) and status from the response. Includes comment aboutas anyusage. - Includes error handling and updates the DB status on failure.
- Takes the validated
handleIncomingSms:- Takes the validated
InfobipIncomingSmsDto. - Iterates through the messages in the payload (Infobip webhooks can batch messages).
- Checks for duplicates based on
externalId(InfobipmessageId). - Creates a new DB record for each unique incoming message with
direction: INBOUND. Includes comment aboutas anyusage. - Includes error handling for individual message processing.
- Takes the validated
5.4. Implement Messaging Module:
// 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.
// 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.
// 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/sendendpoint. - Uses
@UsePipes(new ValidationPipe(...))to automatically validate the incoming request body against theSendSmsDto.transform: trueattempts to convert plain JS objects to DTO instances.whitelist: truestrips 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:
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.