This guide provides a complete walkthrough for building a robust bulk SMS broadcasting service using NestJS and the Infobip API. We will create a backend application capable of accepting lists of phone numbers and a message, then efficiently dispatching these messages via Infobip's communication platform.
This solution addresses the need for applications to send notifications, alerts, or marketing messages to multiple recipients simultaneously, leveraging Infobip's scalable infrastructure. We'll focus on creating a reliable, maintainable, and secure service suitable for production environments.
Prerequisites:
- Node.js (LTS version recommended) and npm or yarn
- An active Infobip account with API access
- Basic understanding of TypeScript, Node.js, NestJS concepts, and REST APIs
- Docker (optional, for containerized deployment)
- A code editor (e.g., VS Code)
- A tool for making API requests (e.g.,
curl
, Postman)
Technology Stack:
- NestJS: A progressive Node.js framework for building efficient, reliable server-side applications. Its modular architecture and built-in features (dependency injection, validation, configuration) accelerate development.
- Infobip API & Node.js SDK (
@infobip-api/sdk
): Provides the interface to Infobip's SMS sending capabilities. We use the official SDK for ease of integration. - TypeScript: Enhances JavaScript with static typing for better code quality and maintainability.
- Dotenv /
@nestjs/config
: For managing environment variables securely. class-validator
/class-transformer
: For robust request data validation.@nestjs/throttler
: For rate limiting API requests.- Winston / NestJS Logger /
nestjs-pino
: For structured logging. helmet
: For basic security headers.p-retry
: For implementing retry logic on API calls.
System Architecture:
(Note: A graphical diagram illustrating this flow is recommended.)
The system involves a client (like Postman or another application) sending a POST request to the /broadcast
endpoint of the NestJS API Gateway (specifically, the BroadcastController
). This controller validates the request and calls the InfobipService
. The InfobipService
handles the core logic, using the Infobip Node.js SDK (@infobip-api/sdk
) to interact with the external Infobip API, which ultimately sends the SMS messages. Configuration is managed via .env
files loaded by @nestjs/config
, and various dependencies like validation, rate limiting, security headers, and retry mechanisms support the core functionality. The API Gateway returns a response (e.g., 202 Accepted with a bulkId
) back to the client.
Final Outcome:
By the end of this guide, you will have a NestJS application with a single API endpoint (POST /broadcast
) that:
- Accepts a list of phone numbers and a message payload.
- Validates the input data.
- Rate-limits incoming requests.
- Uses the Infobip Node.js SDK to send the message to all specified recipients in a single API call (bulk send).
- Handles potential errors gracefully, including retries.
- Provides structured logs for monitoring.
- Returns the
bulkId
provided by Infobip for tracking purposes.
1. Setting up the Project
Let's initialize our NestJS project and install the necessary dependencies.
-
Install NestJS CLI (if you haven't already):
npm install -g @nestjs/cli # OR yarn global add @nestjs/cli
-
Create a new NestJS project:
nest new infobip-bulk-sender
Choose your preferred package manager (npm or yarn) when prompted.
-
Navigate into the project directory:
cd infobip-bulk-sender
-
Install required dependencies:
@infobip-api/sdk
: The official Infobip SDK for Node.js.@nestjs/config
: For managing environment variables (integrates well withdotenv
).@nestjs/throttler
: For implementing rate limiting.class-validator
&class-transformer
: Peer dependencies for@nestjs/common
's built-inValidationPipe
.helmet
: Middleware for setting security-related HTTP headers.p-retry
: Utility for retrying asynchronous operations.dotenv
: Loads environment variables from a.env
file.
npm install @infobip-api/sdk @nestjs/config @nestjs/throttler class-validator class-transformer helmet p-retry dotenv # OR yarn add @infobip-api/sdk @nestjs/config @nestjs/throttler class-validator class-transformer helmet p-retry dotenv
-
Set up Environment Variables: Create a
.env
file in the project root for your Infobip credentials and application settings. Never commit this file to version control.# .env # Application Port PORT=3000 # Infobip API Credentials # Obtain from your Infobip account dashboard (e.g., under API Keys) # Base URL might look like: youraccount.api.infobip.com INFOBIP_BASE_URL=your_infobip_base_url.api.infobip.com INFOBIP_API_KEY=your_infobip_api_key # Default Sender ID (Alphanumeric, Short Code, or Long Number) # Ensure this is registered/approved in your Infobip account if required INFOBIP_SENDER_ID=YourSenderID
Also, create a
.env.example
file to track required variables:# .env.example PORT=3000 INFOBIP_BASE_URL= INFOBIP_API_KEY= INFOBIP_SENDER_ID=
Add
.env
to your.gitignore
file if it's not already there. -
Configure Core Modules (
app.module.ts
): Updatesrc/app.module.ts
to import and configure theConfigModule
andThrottlerModule
.// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { APP_GUARD } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; // We will create these modules/services later import { InfobipModule } from './infobip/infobip.module'; import { BroadcastModule } from './broadcast/broadcast.module'; @Module({ imports: [ // Load environment variables from .env file ConfigModule.forRoot({ isGlobal: true, // Make ConfigService available globally envFilePath: '.env', }), // Configure rate limiting: 10 requests per 60 seconds per IP ThrottlerModule.forRoot([{ ttl: 60000, // Time-to-live in milliseconds (60 seconds) limit: 10, // Max requests per TTL interval }]), // Import our feature modules (to be created) InfobipModule, BroadcastModule, ], controllers: [AppController], providers: [ AppService, // Apply the ThrottlerGuard globally to all routes { provide: APP_GUARD, useClass: ThrottlerGuard, }, ], }) export class AppModule {}
ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' })
: Loads variables from.env
and makesConfigService
available everywhere without needing to importConfigModule
in other modules.ThrottlerModule.forRoot(...)
: Sets up default rate limiting rules. We allow 10 requests per minute per IP address. This protects our API from abuse.{ provide: APP_GUARD, useClass: ThrottlerGuard }
: Applies the rate limiting guard globally.
-
Update Main Application File (
main.ts
): Modifysrc/main.ts
to enable theValidationPipe
globally, usehelmet
, enable CORS, and use the configured port.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ConfigService } from '@nestjs/config'; import { Logger, ValidationPipe } from '@nestjs/common'; import helmet from 'helmet'; // Import helmet async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); const port = configService.get<number>('PORT', 3000); // Default to 3000 if PORT not set // Enable Helmet for basic security headers app.use(helmet()); // Enable CORS if needed (adjust origins for production) app.enableCors({ origin: '*', // Replace with specific origins in production methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, }); // Apply validation pipe globally app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not defined in DTO forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are sent transform: true, // Automatically transform payloads to DTO instances transformOptions: { enableImplicitConversion: true, // Allow basic type conversions }, })); // Use NestJS logger (Consider replacing with PinoLogger if using nestjs-pino) const logger = new Logger('Bootstrap'); await app.listen(port); logger.log(`Application listening on port ${port}`); logger.log(`Infobip Base URL configured: ${configService.get('INFOBIP_BASE_URL')}`); // Log for verification } bootstrap();
app.get(ConfigService)
: Retrieves theConfigService
instance.configService.get<number>('PORT', 3000)
: Gets thePORT
environment variable, defaulting to 3000.app.use(helmet())
: Adds various HTTP headers to improve security (e.g., X-Frame-Options, HSTS). We installedhelmet
in Step 4.app.enableCors()
: Enables Cross-Origin Resource Sharing. Configure appropriately for your frontend's origin in production.app.useGlobalPipes(new ValidationPipe(...))
: Enables automatic request validation based on DTOs decorated withclass-validator
. This is crucial for API robustness.
2. Implementing Core Functionality (Infobip Service)
We'll encapsulate the Infobip SDK interaction within a dedicated NestJS service.
-
Generate the Infobip Module and Service:
nest generate module infobip nest generate service infobip --no-spec
This creates
src/infobip/infobip.module.ts
andsrc/infobip/infobip.service.ts
. The--no-spec
flag skips generating test files for now. -
Implement the
InfobipService
: Editsrc/infobip/infobip.service.ts
.// src/infobip/infobip.service.ts import { Injectable, Logger, InternalServerErrorException, BadGatewayException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; import pRetry from 'p-retry'; // Import p-retry for retries @Injectable() export class InfobipService { private readonly logger = new Logger(InfobipService.name); private infobipClient: Infobip; private defaultSenderId: string; constructor(private configService: ConfigService) { const apiKey = this.configService.get<string>('INFOBIP_API_KEY'); const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL'); this.defaultSenderId = this.configService.get<string>('INFOBIP_SENDER_ID'); if (!apiKey || !baseUrl || !this.defaultSenderId) { this.logger.error('Infobip API Key, Base URL, or Sender ID missing in configuration.'); throw new InternalServerErrorException('Infobip service configuration is incomplete.'); } try { this.infobipClient = new Infobip({ baseUrl: baseUrl, apiKey: apiKey, authType: AuthType.ApiKey, // Using API Key authentication }); this.logger.log('Infobip client initialized successfully.'); } catch (error) { this.logger.error('Failed to initialize Infobip client', error.stack); throw new InternalServerErrorException('Failed to initialize Infobip client'); } } /** * Sends a single SMS message to multiple recipients using the Infobip API, with retries. * @param recipients - Array of phone numbers in E.164 format (e.g., '447123456789'). * @param messageText - The text content of the SMS message. * @param senderId - Optional sender ID to override the default. * @returns The response data from the Infobip API, containing bulkId and message statuses. * @throws BadGatewayException if the Infobip API call fails after retries. */ async sendBulkSms(recipients: string[], messageText: string, senderId?: string): Promise<any> { if (!recipients || recipients.length === 0) { this.logger.warn('Attempted to send bulk SMS with no recipients.'); // Consider throwing BadRequestException or returning an appropriate structure return { success: false, message: 'No recipients provided.' }; } // Deduplicate recipients const uniqueRecipients = [...new Set(recipients)]; if (uniqueRecipients.length < recipients.length) { this.logger.warn(`Removed ${recipients.length - uniqueRecipients.length} duplicate recipients.`); } // Maps the `recipients` array to the `destinations` format required by Infobip ([{ to: string }_ ...]) const destinations = uniqueRecipients.map(recipient => ({ to: recipient })); const effectiveSenderId = senderId || this.defaultSenderId; const payload = { messages: [ { destinations: destinations, from: effectiveSenderId, text: messageText, }, ], // bulkId: `my-custom-bulk-${Date.now()}`, // Optional: Provide a custom bulk ID }; this.logger.log(`Attempting to send bulk SMS to ${uniqueRecipients.length} unique recipients via Infobip...`); this.logger.debug(`Payload: ${JSON.stringify(payload)}`); // Log payload only in debug mode try { const runInfobipCall = async () => { this.logger.debug('Executing Infobip API call attempt...'); // This is the operation we want to retry const response = await this.infobipClient.channels.sms.send(payload); // Optional: Check response.data for partial failures that might warrant a retry, // though typically Infobip accepts the bulk request or rejects it entirely. // If the SDK call succeeds, Infobip has accepted the request. return response; }; // Wrap the API call with p-retry const response = await pRetry(runInfobipCall, { retries: 3, // Number of retries (total 4 attempts) minTimeout: 1000, // Initial delay in ms (1 second) factor: 2, // Exponential backoff factor (1s, 2s, 4s) onFailedAttempt: error => { this.logger.warn( `Infobip API call attempt ${error.attemptNumber} failed. Retries left: ${error.retriesLeft}. Error: ${error.message}` ); // Decide if retry is worthwhile. Don't retry client errors (4xx). // Infobip SDK error structure might vary, inspect 'error.response' if available. const statusCode = error.response?.status || error.code; // Example check if (statusCode && statusCode >= 400 && statusCode < 500) { this.logger.error(`Client-side error detected (${statusCode}). Aborting retries.`); throw error; // Prevent further retries for client errors } }_ }); this.logger.log(`Infobip API call successful. Bulk ID: ${response.data.bulkId}`); this.logger.verbose(`Infobip Full Response: ${JSON.stringify(response.data)}`); // Log full response verbosely // You might want to check response.data.messages for individual statuses // For simplicity_ we return the whole data object here. return response.data; } catch (error) { // This catch block now handles errors after all retries have failed_ // or if a non-retryable error occurred. this.logger.error(`Failed to send bulk SMS via Infobip after retries. Error: ${error.message}`_ error.stack); // Log specific Infobip error details if available if (error.response?.data) { this.logger.error(`Infobip Error Details: ${JSON.stringify(error.response.data)}`); // Extract a more specific message if possible const errorMessage = error.response.data?.requestError?.serviceException?.text || error.message; throw new BadGatewayException(`Infobip API Error: ${errorMessage}`); } // Rethrow a generic error if no specific details are available throw new BadGatewayException(`Failed to communicate with Infobip API after retries: ${error.message}`); } } // --- Alternative Approaches (Conceptual) --- // 1. Sending Personalized Messages in Bulk: // Structure the payload with multiple `messages` objects if needed. // async sendPersonalizedBulkSms(messages: { recipient: string; text: string }[]) { ... } // 2. Handling Very Large Lists (Queueing): // Use BullMQ or similar for asynchronous processing (See Section 6). }
- Constructor: Initializes the
Infobip
client using credentials fromConfigService
. sendBulkSms
Method:- Includes basic recipient deduplication.
- Maps recipients to the correct
destinations
format[{ to: string }_ ...]
. - Constructs the Infobip payload.
- Wraps the
this.infobipClient.channels.sms.send
call withinpRetry
for resilience against transient errors. - Logs attempts_ success_ and detailed errors.
- Throws
BadGatewayException
if the call fails after all retries.
- Constructor: Initializes the
-
Update the
InfobipModule
: Make sure theInfobipService
is provided and exported.// src/infobip/infobip.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { InfobipService } from './infobip.service'; @Module({ imports: [ConfigModule]_ // Make ConfigService available within this module providers: [InfobipService]_ exports: [InfobipService]_ // Export service for other modules to use }) export class InfobipModule {}
3. Building the API Layer (Broadcast Controller)
Now_ let's create the API endpoint.
-
Generate the Broadcast Module and Controller:
nest generate module broadcast nest generate controller broadcast --no-spec
-
Create a Data Transfer Object (DTO) for Validation: Create
src/broadcast/dto/create-broadcast.dto.ts
. (Optional: For Swagger documentation_ install@nestjs/swagger swagger-ui-express
and configure it inmain.ts
)# Optional: Install Swagger dependencies npm install @nestjs/swagger swagger-ui-express
// src/broadcast/dto/create-broadcast.dto.ts import { ApiProperty } from '@nestjs/swagger'; // Optional: for Swagger import { IsArray_ IsNotEmpty_ IsString_ ArrayMinSize_ IsPhoneNumber_ IsOptional } from 'class-validator'; // Import IsOptional export class CreateBroadcastDto { @ApiProperty({ description: 'Array of recipient phone numbers in E.164 format'_ example: ['+447123456789'_ '+14155552671']_ // Use E.164 format examples type: [String]_ }) @IsArray() @ArrayMinSize(1) // Apply IsPhoneNumber validation to each element in the array. // Provides basic structure check. For robust E.164 validation across regions_ // consider libphonenumber-js (npm install libphonenumber-js) and a custom validator. // Ensure data is pre-formatted to E.164 before sending to this API or handle normalization. @IsPhoneNumber(undefined_ { each: true_ message: 'Each recipient must be a valid phone number_ preferably in E.164 format (e.g._ +447123456789)' }) recipients: string[]; @ApiProperty({ description: 'The text message content to send'_ example: 'Hello from our service!'_ }) @IsString() @IsNotEmpty() message: string; @ApiProperty({ description: 'Optional: Sender ID to use for this broadcast_ overriding the default.'_ example: 'InfoSMS'_ required: false_ }) @IsString() @IsNotEmpty() @IsOptional() // Make senderId optional senderId?: string; }
- Uses
class-validator
decorators (@IsArray
_@IsString
_@IsNotEmpty
_@ArrayMinSize
_@IsPhoneNumber
_@IsOptional
). @IsPhoneNumber
: Provides basic validation. See Section 6 for more on robust validation.@IsOptional
: MakessenderId
optional.@ApiProperty
: Added for potential Swagger integration.
- Uses
-
Implement the
BroadcastController
: Editsrc/broadcast/broadcast.controller.ts
.// src/broadcast/broadcast.controller.ts import { Controller_ Post_ Body_ HttpCode_ HttpStatus_ Logger } from '@nestjs/common'; import { Throttle } from '@nestjs/throttler'; import { InfobipService } from '../infobip/infobip.service'; import { CreateBroadcastDto } from './dto/create-broadcast.dto'; // Correct import path @Controller('broadcast') // Route prefix: /broadcast export class BroadcastController { private readonly logger = new Logger(BroadcastController.name); constructor( private readonly infobipService: InfobipService_ ) {} @Post() @HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted @Throttle({ default: { limit: 5_ ttl: 60000 } }) // Override global throttle: 5 requests/min async sendBroadcast(@Body() createBroadcastDto: CreateBroadcastDto) { this.logger.log(`Received broadcast request for ${createBroadcastDto.recipients.length} recipients.`); try { // Call the service method with validated DTO data const result = await this.infobipService.sendBulkSms( createBroadcastDto.recipients_ createBroadcastDto.message_ createBroadcastDto.senderId_ // Pass optional senderId ); // Handle case where service returns early (e.g._ no recipients) // Check for the specific structure returned in that case if (result && result.success === false) { // Could return 400 Bad Request here_ or stick with 202 but include message this.logger.warn(`Broadcast request rejected by service: ${result.message}`); // Adjust status code if needed_ e.g._ HttpStatus.BAD_REQUEST return { statusCode: HttpStatus.BAD_REQUEST_ // Example: Return 400 message: result.message || 'Broadcast request could not be processed.'_ // No bulkId in this case }; } this.logger.log(`Broadcast request processed successfully by Infobip. Bulk ID: ${result.bulkId}`); // Return relevant information return { message: 'Broadcast request accepted for processing by Infobip.'_ bulkId: result.bulkId_ // messages: result.messages // Optionally return individual message statuses }; } catch (error) { // Errors thrown by InfobipService (e.g._ BadGatewayException) will be caught here this.logger.error(`Broadcast request failed: ${error.message}`_ error.stack); // Rethrow the error to let NestJS default exception filter handle it // (e.g._ converts BadGatewayException to a 502 response) throw error; } } }
@Controller('broadcast')
: Defines the base route.@Post()
: Handles POST requests.@HttpCode(HttpStatus.ACCEPTED)
: Sets default success status to 202.@Throttle(...)
: Overrides global rate limit for this endpoint.@Body() createBroadcastDto: CreateBroadcastDto
: Validates the request body using the DTO and globalValidationPipe
.- Injects
InfobipService
. - Calls
infobipService.sendBulkSms
. - Returns the
bulkId
on success. - Rethrows errors from the service layer.
-
Update the
BroadcastModule
: ImportInfobipModule
.// src/broadcast/broadcast.module.ts import { Module } from '@nestjs/common'; import { BroadcastController } from './broadcast.controller'; import { InfobipModule } from '../infobip/infobip.module'; // Import InfobipModule @Module({ imports: [ InfobipModule_ // Make InfobipService available ]_ controllers: [BroadcastController]_ providers: []_ }) export class BroadcastModule {}
-
Testing the API Endpoint: Start the application:
npm run start:dev # OR yarn start:dev
Use
curl
or Postman:Using
curl
: (Replace placeholders)curl -X POST http://localhost:3000/broadcast \ -H "Content-Type: application/json" \ -d '{ "recipients": ["+14155550100"_ "+447123456789"]_ "message": "Test broadcast from NestJS!"_ "senderId": "OptionalSender" }'
Expected Success Response (202 Accepted):
{ "message": "Broadcast request accepted for processing by Infobip."_ "bulkId": "some-unique-bulk-id-from-infobip" }
Example Validation Error Response (400 Bad Request): (If sending invalid data_ e.g._ missing
message
){ "statusCode": 400_ "message": [ "message should not be empty"_ "message must be a string" ]_ "error": "Bad Request" }
4. Integrating with Infobip (Details & Configuration)
Recap of the integration specifics handled in InfobipService
.
-
Obtaining Credentials:
- Log in to your Infobip Portal.
- Navigate to Developers -> API Keys.
- Create/copy your API Key.
- Note your account-specific Base URL (e.g.,
xxxxx.api.infobip.com
). - Check Channels and Numbers -> Sender Names for your
INFOBIP_SENDER_ID
. Ensure it's registered/approved if needed. Free trials often have restrictions.
-
Environment Variables: Ensure these are correctly set in
.env
:INFOBIP_BASE_URL
INFOBIP_API_KEY
INFOBIP_SENDER_ID
-
Secure Handling:
- Use
.env
locally (add to.gitignore
). - Use platform-specific secrets management (AWS Secrets Manager, Kubernetes Secrets, etc.) in production. Never hardcode secrets.
- Use
-
Fallback Mechanisms (Retry):
- The
InfobipService
now usesp-retry
to automatically retry failed API calls (due to network issues or temporary Infobip 5xx errors) with exponential backoff. - A circuit breaker (e.g., using
opossum
) could be added for prolonged outages, preventing repeated calls to a known-failing service.
- The
5. Error Handling, Logging, and Retry Mechanisms
Robustness is key.
-
Error Handling Strategy:
- Validation Errors (400): Handled by
ValidationPipe
. - Rate Limiting Errors (429): Handled by
@nestjs/throttler
. - Configuration Errors (500): Handled in
InfobipService
constructor. - Infobip API Errors (502): Caught in
InfobipService
after retries, logged, and rethrown asBadGatewayException
. - Unexpected Errors (500): Handled by NestJS default exception filter.
- Validation Errors (400): Handled by
-
Logging:
- Uses built-in
Logger
(@nestjs/common
). Logs info, errors, warnings, debug, verbose messages. - Structured Logging (Recommended): For production, use
nestjs-pino
for JSON logs compatible with aggregation systems (Datadog, Splunk, ELK).- Install:
npm install nestjs-pino pino-http pino-pretty # pino-pretty for dev console
- Setup (Conceptual):
// src/main.ts import { Logger } from 'nestjs-pino'; // ... in bootstrap() app.useLogger(app.get(Logger)); // Use PinoLogger instance globally
Replace// src/app.module.ts import { LoggerModule } from 'nestjs-pino'; @Module({ imports: [ // Configure Pino logger (e.g., level, prettyPrint for dev) LoggerModule.forRoot({ pinoHttp: { level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined, // Add custom serializers, redaction paths etc. }, }), // ... other modules ], }) export class AppModule {}
new Logger(...)
calls with dependency injection or ensure the global logger is used correctly.
- Install:
- Uses built-in
-
Retry Mechanisms:
- Implemented in
InfobipService
usingp-retry
(installed in Section 1, used in Section 2). - Retries up to 3 times with exponential backoff (1s, 2s, 4s delays).
- Logs failed attempts.
- Avoids retrying client-side (4xx) errors.
- Implemented in
6. Database Schema and Data Layer (Recommendation)
While not implemented here, persistence is crucial for production.
-
Why a Database? Track broadcast history, statuses, manage recipients, schedule messages, store delivery reports (via webhooks).
-
Recommendation: Use PostgreSQL/MySQL with Prisma or TypeORM.
-
Conceptual Prisma Schema:
// schema.prisma datasource db { provider = ""postgresql"" // or ""mysql"", ""sqlite"" url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" } model Broadcast { id String @id @default(cuid()) bulkId String? @unique // Infobip bulk ID messageText String senderId String status String @default(""PENDING"") // PENDING, SENT, FAILED, PARTIAL createdAt DateTime @default(now()) updatedAt DateTime @updatedAt recipients RecipientStatus[] } model RecipientStatus { id String @id @default(cuid()) broadcast Broadcast @relation(fields: [broadcastId], references: [id]) broadcastId String phoneNumber String // E.164 format messageId String? // Infobip message ID for this recipient status String // e.g., PENDING, SENT, DELIVERED, FAILED, REJECTED statusReason String? updatedAt DateTime @updatedAt @@unique([broadcastId, phoneNumber]) }
-
Implementation: Integrate Prisma/TypeORM into NestJS, create a service to handle database operations (create broadcast record, update statuses), and call this service from the
BroadcastController
and potentially a webhook handler for delivery reports. This is beyond the scope of this initial guide but essential for a complete solution. Consider using a message queue (like BullMQ) for large broadcasts to decouple API requests from database writes and Infobip calls.