This guide provides a comprehensive, step-by-step walkthrough for building a robust bulk SMS broadcast application using Node.js, the Express framework, and the Infobip API. We'll cover everything from project setup and core broadcast logic to error handling, security, deployment, and monitoring, enabling you to create a scalable and reliable messaging solution.
The final application will feature an API endpoint that accepts a list of recipients and a message, then efficiently broadcasts the message via SMS using Infobip, incorporating best practices for performance and resilience.
Project Overview and Goals
What We're Building:
A Node.js backend application with an Express API designed to:
- Accept POST requests containing a list of phone numbers and a message content.
- Securely interact with the Infobip SMS API using their official Node.js SDK.
- Implement a robust broadcasting mechanism capable of handling large recipient lists efficiently through batching and rate limit consideration.
- Store recipient data (optional but recommended for tracking/opt-outs) in a database (using PostgreSQL and Prisma ORM).
- Incorporate essential production features: structured logging, error handling, retry mechanisms, security measures, and basic monitoring.
Problem Solved:
Manually sending SMS messages to large groups is inefficient and error-prone. Existing tools might lack flexibility or specific integration needs. This guide provides the foundation for a custom, scalable solution to send targeted bulk SMS communications programmatically – ideal for notifications, alerts, marketing campaigns, or user engagement.
Technologies Used:
- Node.js: Asynchronous JavaScript runtime for building scalable network applications.
- Express.js: Minimalist and flexible Node.js web application framework for creating the API layer.
- Infobip API & Node.js SDK (
@infobip-api/sdk
): Cloud communications platform providing the SMS sending functionality via a developer-friendly SDK. Chosen for its robust features and clear documentation. - PostgreSQL: Powerful open-source relational database for storing recipient lists and potentially message logs or statuses.
- Prisma: Next-generation Node.js and TypeScript ORM for database access and migrations. Simplifies database interactions.
- dotenv: Loads environment variables from a
.env
file for secure configuration management. - express-validator: Middleware for validating incoming API request data.
- express-rate-limit: Middleware for basic API rate limiting.
- Winston: A versatile logging library for Node.js.
System Architecture:
+-------------+ +---------------------+ +-----------------+ +-----------------+
| API Client |------>| Node.js/Express API |------>| Broadcast Service |------>| Infobip SMS API |
| (e.g., curl,| | (Validation, Auth) | | (Batching, Retry) | +-----------------+
| Postman) | +----------+----------+ +---------+---------+
+-------------+ | |
| | (Optional Logging)
v v
+------------------+ +------------------+
| PostgreSQL DB | | Logging Service |
| (Prisma Client) | | (e.g., Winston) |
+------------------+ +------------------+
Prerequisites:
- Node.js (LTS version recommended) and npm/yarn installed.
- An active Infobip account (Sign up here if you don't have one).
- Access to your Infobip API Key and Base URL.
- Basic familiarity with JavaScript, Node.js, REST APIs, and terminal commands.
- Docker installed (optional, for easy PostgreSQL setup).
- Access to test phone numbers for verification (especially the registered number if using an Infobip free trial).
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
1.1 Create Project Directory and Initialize:
Open your terminal and run the following commands:
mkdir node-infobip-broadcast
cd node-infobip-broadcast
npm init -y
This creates a new directory and initializes a package.json
file with default settings.
1.2 Install Dependencies:
# Core Framework & Utilities
npm install express dotenv @infobip-api/sdk
# Development Dependencies (Optional but Recommended)
npm install -D nodemon typescript @types/node @types/express ts-node
# Database (Prisma & PostgreSQL Driver)
npm install @prisma/client pg
npm install -D prisma
# Validation, Rate Limiting & Logging
npm install express-validator express-rate-limit winston
express
: The web framework.dotenv
: To load environment variables.@infobip-api/sdk
: The official Infobip SDK.nodemon
: Utility to automatically restart the server during development. (Dev dependency)typescript
,@types/*
,ts-node
: For TypeScript support (optional, but recommended for larger projects). We'll use JavaScript in this guide for simplicity, but these are good additions. (Dev dependencies)@prisma/client
: Prisma's database client.pg
: Node.js driver for PostgreSQL.prisma
: Prisma CLI for migrations and studio. (Dev dependency)express-validator
: For API input validation.express-rate-limit
: Basic protection against brute-force/DoS attacks.winston
: For structured logging.
1.3 Configure package.json
Scripts:
Add the following scripts to your package.json
for easier development and execution:
// package.json
{
// ... other configurations
"main": "src/server.js", // Point to our main server file
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js", // Requires nodemon installed globally or via npx
"test": "echo \"Error: no test specified\" && exit 1", // Placeholder for tests
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"prisma:generate": "prisma generate"
},
// ... other configurations
}
1.4 Project Structure:
Create the following directory structure to organize the code:
node-infobip-broadcast/
├── prisma/
│ └── schema.prisma
├── src/
│ ├── config/
│ │ ├── infobipClient.js
│ │ └── logger.js
│ ├── controllers/
│ │ └── broadcastController.js
│ ├── db/
│ │ └── prisma.js
│ ├── middleware/
│ │ ├── errorHandler.js
│ │ └── validators.js
│ ├── routes/
│ │ └── broadcastRoutes.js
│ ├── services/
│ │ └── broadcastService.js
│ └── server.js
├── logs/
├── .env
├── .gitignore
├── package.json
└── package-lock.json
Create the empty files within these directories for now. Remember to manually create the logs
directory: mkdir logs
.
1.5 Setup .env
File:
Create a .env
file in the project root. This file stores sensitive information and configurations. Never commit this file to version control.
# .env
# Infobip API Credentials
INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY
INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL # e.g., xyz123.api.infobip.com
# Database Connection URL (using PostgreSQL)
# Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE
DATABASE_URL="postgresql://user:password@localhost:5432/broadcast_db?schema=public"
# Application Settings
PORT=3000
LOG_LEVEL=info # e.g., error, warn, info, http, verbose, debug, silly
# Broadcast Settings (Adjust as needed)
BROADCAST_BATCH_SIZE=100 # Number of messages per Infobip API call
BROADCAST_BATCH_DELAY_MS=500 # Delay between sending batches (milliseconds)
INFOBIP_SENDER_ID=InfoSMS # Optional: Default or registered sender ID
Important:
- You must replace
YOUR_INFOBIP_API_KEY
andYOUR_INFOBIP_BASE_URL
with your actual credentials obtained from the Infobip portal (see Section 4). - The
DATABASE_URL
is an example. You must update theuser
,password
,localhost
(host),5432
(port), andbroadcast_db
(database name) parts to match your specific PostgreSQL database setup. BROADCAST_BATCH_SIZE
andBROADCAST_BATCH_DELAY_MS
control how the bulk sending is split into smaller requests. Adjust based on Infobip's recommendations or your plan limits.INFOBIP_SENDER_ID
is optional; if you have a registered alphanumeric sender ID, set it here.
1.6 Setup .gitignore
:
Create a .gitignore
file in the root directory to prevent committing sensitive files and unnecessary folders:
# .gitignore
# Dependencies
node_modules/
# Environment variables
.env
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Prisma
prisma/migrations/*/*.sql # Avoid committing generated SQL if desired
# Build outputs (if using TypeScript/bundlers)
dist/
build/
# OS generated files
.DS_Store
Thumbs.db
2. Implementing Core Functionality (Bulk Broadcasting)
The core logic resides in the BroadcastService
. It takes a list of recipients and a message, batches them, and sends them via the Infobip SDK, handling potential errors for each batch.
2.1 Configure Logger:
Setup a reusable Winston logger. Ensure you have created the logs
directory in your project root (mkdir logs
), or the file transport will fail.
// src/config/logger.js
const winston = require('winston');
require('dotenv').config();
// Ensure logs directory exists (optional defensive check)
const fs = require('fs');
const path = require('path');
const logDir = 'logs';
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }), // Log stack traces
winston.format.splat(),
winston.format.json() // Log in JSON format
),
defaultMeta: { service: 'broadcast-service' },
transports: [
// - Write all logs with importance level of `error` or less to `error.log`
// - Write all logs with importance level of `info` or less to `combined.log`
new winston.transports.File({ filename: path.join(logDir, 'error.log'), level: 'error' }),
new winston.transports.File({ filename: path.join(logDir, 'combined.log') }),
],
});
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}));
}
module.exports = logger;
- This sets up logging to files (
logs/error.log
,logs/combined.log
) and the console (during development).
2.2 Configure Infobip Client:
Create a singleton instance of the Infobip client.
// src/config/infobipClient.js
const { Infobip, AuthType } = require('@infobip-api/sdk');
require('dotenv').config();
const logger = require('./logger');
if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) {
logger.error('FATAL: Infobip API Key or Base URL missing in environment variables (.env). Application cannot start.');
// In a real application, prevent startup if core config is missing
process.exit(1);
}
let infobipClient;
try {
infobipClient = new Infobip({
baseUrl: process.env.INFOBIP_BASE_URL,
apiKey: process.env.INFOBIP_API_KEY,
authType: AuthType.ApiKey, // Using API Key authentication
});
logger.info('Infobip client configured successfully.');
} catch (error) {
logger.error('FATAL: Failed to initialize Infobip client.', error);
process.exit(1);
}
module.exports = infobipClient;
- Initializes the SDK using credentials from
.env
. - Includes a check to ensure credentials are provided and exits if they are missing.
2.3 Implement the Broadcast Service:
This service contains the main logic for sending messages in batches.
// src/services/broadcastService.js
const infobipClient = require('../config/infobipClient');
const logger = require('../config/logger');
require('dotenv').config();
// Configuration for batching and delays from .env or defaults
const BATCH_SIZE = parseInt(process.env.BROADCAST_BATCH_SIZE, 10) || 100;
const BATCH_DELAY_MS = parseInt(process.env.BROADCAST_BATCH_DELAY_MS, 10) || 500;
const SENDER_ID = process.env.INFOBIP_SENDER_ID || 'InfoSMS'; // Default or from .env
/**
* Helper function to introduce delays
* @param {number} ms - Milliseconds to wait
*/
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* Sends a single batch of SMS messages using Infobip API.
* This function can be enhanced with retry logic (see Section 5.4).
* @param {Array<string>} batchRecipients - Array of phone numbers for this batch.
* @param {string} messageText - The SMS message content.
* @returns {Promise<object>} - Promise resolving to the Infobip API response for the batch or an error object.
*/
const sendSmsBatch = async (batchRecipients, messageText) => {
const destinations = batchRecipients.map(recipient => ({ to: recipient }));
const payload = {
messages: [
{
from: SENDER_ID, // Registered Sender ID or default
destinations: destinations,
text: messageText,
},
],
// bulkId: 'YOUR_CUSTOM_BULK_ID', // Optional: Assign a custom ID for the entire broadcast
// tracking: { ... } // Optional: Add tracking parameters
};
logger.info(`Sending batch of ${destinations.length} messages.`);
try {
// Note: The SDK's send method handles structuring the request correctly.
// This approach sends one API request per batch.
const infobipResponse = await infobipClient.channels.sms.send(payload);
logger.info(`Batch sent successfully. Bulk ID: ${infobipResponse.data.bulkId}, Messages: ${infobipResponse.data.messages.length}`);
// Log individual message statuses if needed for debugging
infobipResponse.data.messages.forEach(msg => {
logger.debug(`Message to ${msg.to}: Status ${msg.status.groupName} (${msg.status.name}), ID: ${msg.messageId}`);
});
return { success: true, response: infobipResponse.data };
} catch (error) {
// Log detailed error information from Infobip if available
const errorDetails = error.response?.data || { message: error.message, status: error.response?.status };
logger.error(`Failed to send batch. Error: ${error.message}`, {
status: error.response?.status,
infobipError: errorDetails,
recipientsCount: batchRecipients.length,
});
// Return structured error for the controller
return { success: false, error: errorDetails };
}
};
/**
* Broadcasts an SMS message to multiple recipients in batches.
* @param {Array<string>} recipients - Array of phone numbers (E.164 format recommended).
* @param {string} messageText - The SMS message content.
* @returns {Promise<object>} - An object summarizing the broadcast results.
*/
const broadcastSms = async (recipients, messageText) => {
logger.info(`Starting broadcast to ${recipients.length} recipients. Batch size: ${BATCH_SIZE}, Delay: ${BATCH_DELAY_MS}ms`);
const results = {
totalRecipients: recipients.length,
successfulBatches: 0,
failedBatches: 0,
totalMessagesSentAttempted: 0, // Based on number of recipients in successfully *initiated* batches
errors: [], // Stores details of failed batches
batchResponses: [], // Stores raw responses from Infobip for each batch (success or failure)
};
for (let i = 0; i < recipients.length; i += BATCH_SIZE) {
const batch = recipients.slice(i_ i + BATCH_SIZE);
const batchIndex = Math.floor(i / BATCH_SIZE) + 1;
const totalBatches = Math.ceil(recipients.length / BATCH_SIZE);
logger.debug(`Processing batch ${batchIndex} of ${totalBatches}`);
// Use the sendSmsBatch function (which might include retries - see Section 5.4)
const batchResult = await sendSmsBatch(batch_ messageText);
results.batchResponses.push(batchResult); // Store raw batch result
if (batchResult.success) {
results.successfulBatches++;
results.totalMessagesSentAttempted += batch.length; // Count recipients in successful attempts
} else {
results.failedBatches++;
results.errors.push({
batchIndex: batchIndex -1_ // 0-based index for consistency if needed elsewhere
errorDetails: batchResult.error_
recipientsInBatch: batch // Include recipients for retry/debugging if needed
});
}
// Add delay between batches if not the last batch and delay is configured
if (i + BATCH_SIZE < recipients.length && BATCH_DELAY_MS > 0) {
logger.debug(`Waiting ${BATCH_DELAY_MS}ms before next batch...`);
await delay(BATCH_DELAY_MS);
}
}
logger.info(`Broadcast finished. Successful Batches: ${results.successfulBatches}, Failed Batches: ${results.failedBatches}, Total Attempted Messages in Successful Batches: ${results.totalMessagesSentAttempted}`);
return results;
};
module.exports = {
broadcastSms,
sendSmsBatch // Exporting for potential retry enhancements
};
- Batching: Splits the
recipients
array into smaller chunks defined byBATCH_SIZE
. - Delay: Introduces a pause (
BATCH_DELAY_MS
) between sending batches to avoid hitting rate limits. - Error Handling per Batch: Each batch is sent independently. If one fails, the service logs the error and continues with the next batch.
- Structured Response: Returns a summary object detailing the success/failure counts and any errors encountered.
- Sender ID: Uses a default 'InfoSMS' or allows configuration via
INFOBIP_SENDER_ID
in.env
. Using a registered alphanumeric sender ID often improves deliverability and branding (requires setup in Infobip). - Logging: Logs key events and errors during the broadcast process.
3. Building the API Layer
We'll use Express to create an API endpoint that triggers the broadcast service.
3.1 Implement Request Validation:
Use express-validator
to ensure incoming requests have the correct structure.
// src/middleware/validators.js
const { body, validationResult } = require('express-validator');
const validateBroadcastRequest = [
// Validate 'recipients' field
body('recipients')
.isArray({ min: 1 })
.withMessage('Recipients must be an array with at least one phone number.'),
body('recipients.*')
.isString()
.withMessage('Each recipient must be a string.')
// Basic E.164 format check: optional '+', followed by 1-3 digits (country code),
// then 6-14 digits (number). Adjust as needed.
// Examples matched: +14155552671, 447123456789
// Note: This is a basic regex and might not cover all valid international formats.
// Infobip's API provides more robust validation.
.matches(/^\\+?[1-9]\\d{1,14}$/)
.withMessage('Each recipient must be a string resembling an international phone number (e.g., +14155552671 or 447123456789).'),
// Validate 'message' field
body('message')
.isString()
.withMessage('Message must be a string.')
.trim()
.notEmpty()
.withMessage('Message cannot be empty.')
.isLength({ max: 1600 }) // Standard SMS limits vary, 1600 allows for multi-part messages
.withMessage('Message exceeds maximum length (1600 characters for potential multi-part SMS).'),
// Middleware to handle validation results
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
// Log validation errors for server visibility
console.error('Validation Errors:', errors.array());
return res.status(400).json({ errors: errors.array() });
}
next();
},
];
module.exports = {
validateBroadcastRequest,
};
- Defines rules for the
recipients
array (must exist, be an array, contain valid-looking phone number strings) and themessage
string (must exist, be non-empty, within length limits). - The phone number regex (
/^\\+?[1-9]\\d{1,14}$/
) is a basic check. It allows an optional+
followed by digits, starting with 1-9. It matches common formats but isn't exhaustive for all global numbering plans. Rely on Infobip's API for definitive validation.
3.2 Create the Broadcast Controller:
This handles the incoming request, calls the service, and sends the response.
// src/controllers/broadcastController.js
const broadcastService = require('../services/broadcastService');
const logger = require('../config/logger');
const handleBroadcastRequest = async (req, res, next) => {
const { recipients, message } = req.body;
// Input already validated by middleware
try {
logger.info(`Received broadcast request for ${recipients.length} recipients.`);
const broadcastResult = await broadcastService.broadcastSms(recipients, message);
// Determine the overall status based on batch results
let statusCode = 200; // Default to OK
let responseStatus = 'success';
let responseMessage = 'Broadcast initiated successfully.';
if (broadcastResult.failedBatches > 0) {
if (broadcastResult.successfulBatches === 0) {
// All batches failed
statusCode = 500; // Internal Server Error (as the core task failed)
responseStatus = 'error';
responseMessage = 'Broadcast failed completely. See details for errors.';
} else {
// Some batches failed (partial success)
statusCode = 207; // Multi-Status
responseStatus = 'partial_success';
responseMessage = 'Broadcast partially completed with some errors. See details.';
}
} else if (recipients.length === 0) {
// Edge case: Request was valid but no recipients to send to (e.g., after filtering)
// This check might be better placed in the service depending on logic.
responseMessage = 'Broadcast request processed, but no recipients were targeted.';
}
res.status(statusCode).json({
status: responseStatus,
message: responseMessage,
details: broadcastResult,
});
} catch (error) {
// Catch unexpected errors NOT handled by the service's batch processing
logger.error('Unhandled error in broadcast controller:', error);
// Pass to the generic error handler middleware
next(error); // Let the central error handler manage the response
}
};
module.exports = {
handleBroadcastRequest,
};
- Extracts validated data from the request body.
- Calls
broadcastService.broadcastSms
. - Sends an appropriate HTTP status code (200 OK, 207 Multi-Status, 500 Internal Server Error) and JSON response based on the success/failure of the broadcast batches.
3.3 Define API Routes:
Set up the Express router for the broadcast endpoint.
// src/routes/broadcastRoutes.js
const express = require('express');
const broadcastController = require('../controllers/broadcastController');
const { validateBroadcastRequest } = require('../middleware/validators');
const rateLimit = require('express-rate-limit'); // Import rate limiter
const router = express.Router();
// Apply rate limiting to the broadcast endpoint
// Adjust limits based on expected usage and Infobip's constraints
const broadcastLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20, // Limit each IP to 20 broadcast requests per windowMs (adjust as needed)
message: { // Send JSON response on rate limit
status: 'error',
message: 'Too many broadcast requests initiated from this IP, please try again after 15 minutes'
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// POST /api/v1/broadcast
// Body: { "recipients": ["+1...", "+44..."], "message": "Hello world!" }
router.post(
'/',
broadcastLimiter, // 1. Apply rate limiting
validateBroadcastRequest, // 2. Validate input
broadcastController.handleBroadcastRequest // 3. Handle the request
);
module.exports = router;
- Defines a
POST
route at/api/v1/broadcast
. - Applies the rate limiter and validation middleware before the controller logic executes.
3.4 Create the Main Server File:
Tie everything together in server.js
.
// src/server.js
require('dotenv').config(); // Load .env variables early
const express = require('express');
const broadcastRoutes = require('./routes/broadcastRoutes');
const logger = require('./config/logger');
const errorHandler = require('./middleware/errorHandler'); // Import error handler
const prisma = require('./db/prisma'); // Import prisma client instance
const app = express();
const PORT = process.env.PORT || 3000;
// --- Database Connection Check (Optional but recommended on startup) ---
async function checkDatabaseConnection() {
try {
// Prisma Client automatically handles connection pooling, $connect isn't strictly needed
// but useful for an initial check. Use a simple query.
await prisma.$queryRaw`SELECT 1`;
logger.info('Database connection successful.');
} catch (error) {
logger.error('FATAL: Database connection failed on startup:', error);
process.exit(1); // Exit if DB connection fails
}
}
// Uncomment the line below if you want the application to exit on DB connection failure at startup
// checkDatabaseConnection();
// -----------------------------------------------------------
// --- Middleware ---
app.use(express.json({ limit: '1mb' })); // Parse JSON request bodies, limit payload size
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// Basic Logging Middleware (Log incoming requests)
app.use((req, res, next) => {
// Log basic request info. Avoid logging sensitive headers/body in production.
logger.http(`Incoming Request: ${req.method} ${req.originalUrl}`, { ip: req.ip });
res.on('finish', () => {
logger.http(`Request Completed: ${res.statusCode} ${res.statusMessage}; ${res.get('Content-Length') || 0}b sent`);
});
next();
});
// -----------------
// --- Routes ---
app.use('/api/v1/broadcast', broadcastRoutes);
// Health Check Endpoint
app.get('/health', (req, res) => {
// Basic health check. Could be expanded to check DB/Infobip connectivity.
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// -------------
// --- Error Handling ---
// Catch 404s (requests for routes not defined) and forward to error handler
app.use((req, res, next) => {
const error = new Error('Not Found - The requested resource does not exist.');
error.status = 404;
next(error); // Pass the error to the next middleware (errorHandler)
});
// Use the generic error handler middleware (must be the LAST middleware registered)
app.use(errorHandler);
// --------------------
// --- Server Start ---
const server = app.listen(PORT, () => {
logger.info(`Server running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`);
logger.info(`API available at http://localhost:${PORT}/api/v1/broadcast`);
});
// Graceful Shutdown Logic
const signals = {
'SIGHUP': 1,
'SIGINT': 2,
'SIGTERM': 15
};
const shutdown = async (signal, value) => {
logger.warn(`Received signal ${signal}. Shutting down gracefully...`);
server.close(async () => {
logger.info('HTTP server closed.');
try {
await prisma.$disconnect(); // Disconnect Prisma client
logger.info('Database connection closed.');
} catch (e) {
logger.error('Error disconnecting database:', e);
} finally {
logger.info('Shutdown complete.');
process.exit(128 + value);
}
});
// Force shutdown after a timeout
setTimeout(() => {
logger.error('Graceful shutdown timed out, forcing exit.');
process.exit(128 + value);
}, 10000).unref(); // 10 seconds timeout, unref() allows Node to exit if shutdown is faster
};
Object.keys(signals).forEach((signal) => {
process.on(signal, () => shutdown(signal, signals[signal]));
});
// -----------------
- Loads environment variables.
- Initializes Express.
- Connects middleware (
express.json
, request logger). - Mounts the broadcast API routes under
/api/v1/broadcast
. - Adds a
/health
endpoint for monitoring. - Sets up 404 handling and the generic error handler (defined in Section 5).
- Starts the server and includes improved graceful shutdown logic for SIGINT/SIGTERM.
- Includes an optional database connection check on startup.
3.5 Testing the API Endpoint:
Once the server is running (npm run dev
), you can test the endpoint using curl
or Postman:
Curl Example:
curl -X POST http://localhost:3000/api/v1/broadcast \
-H "Content-Type: application/json" \
-d '{
"recipients": ["+14155550100", "+447123456789"],
"message": "This is a test broadcast message from Node.js!"
}'
Note: The phone numbers +14155550100
and +447123456789
are examples. Replace them with actual phone numbers you can test with. If using an Infobip free trial, you can likely only send messages to the phone number you verified during signup.
Expected Successful Response (Status 200):
{
"status": "success",
"message": "Broadcast initiated successfully.",
"details": {
"totalRecipients": 2,
"successfulBatches": 1,
"failedBatches": 0,
"totalMessagesSentAttempted": 2,
"errors": [],
"batchResponses": [
{
"success": true,
"response": {
"bulkId": "some-unique-bulk-id", // Actual ID from Infobip
"messages": [
{
"to": "+14155550100",
"status": { "groupId": 1, "groupName": "PENDING", "id": 7, "name": "PENDING_ENROUTE", "description": "Message sent to next instance" }, // Actual status
"messageId": "some-unique-message-id-1" // Actual ID
},
{
"to": "+447123456789",
"status": { "groupId": 1, "groupName": "PENDING", "id": 7, "name": "PENDING_ENROUTE", "description": "Message sent to next instance" }, // Actual status
"messageId": "some-unique-message-id-2" // Actual ID
}
]
}
}
]
}
}
Example Validation Error Response (Status 400):
{
"errors": [
{
"type": "field",
"value": "invalid-number",
"msg": "Each recipient must be a string resembling an international phone number (e.g., +14155552671 or 447123456789).",
"path": "recipients[1]",
"location": "body"
}
// ... other errors if applicable
]
}
Example Rate Limit Response (Status 429):
{
"status": "error",
"message": "Too many broadcast requests initiated from this IP, please try again after 15 minutes"
}
4. Integrating with Infobip
This section details obtaining credentials and configuring the SDK.
4.1 Obtaining Infobip API Key and Base URL:
- Log in to your Infobip account portal.
- Navigate to the API Keys section. This is typically found under your account settings, developer tools, or a dedicated API section. (The exact path might change, refer to Infobip documentation if needed).
- Create a new API Key: Give it a descriptive name (e.g., ""Node Broadcast App""). Ensure it has the necessary permissions (e.g., SMS sending).
- Copy the API Key: Securely store this key. It will be required for authentication. Treat this like a password.
- Find your Base URL: This unique URL is assigned to your account for API requests. It's usually displayed prominently on the API dashboard or near where you generated the API key.