code examples
code examples
Build a Bulk SMS Broadcast System with Node.js, Express & Infobip API
Learn how to build a production-ready bulk SMS broadcast system using Node.js, Express, and Infobip API. Complete tutorial with code examples, security best practices, and deployment.
Build a Bulk SMS Broadcast System with Node.js, Express & Infobip API
Learn how to build a production-ready bulk SMS broadcast system using Node.js, Express, and the Infobip API. This comprehensive guide covers project setup, core functionality, error handling, security, monitoring, and deployment best practices.
By the end of this tutorial, you'll have a functional Express application that sends SMS messages to multiple recipients using Infobip's bulk sending API – perfect for reliably broadcasting alerts, notifications, or marketing messages at scale.
Technologies Used:
- Node.js: JavaScript runtime for server-side applications
- Express: Minimal Node.js web framework
- Infobip: Cloud communications platform with SMS API (via official Node.js SDK)
- dotenv: Secure environment variable management
- Prisma: Next-generation ORM for Node.js and TypeScript
- PostgreSQL: Open-source relational database (MySQL and MariaDB are compatible alternatives with minor code changes)
- Jest & Supertest: Unit and integration testing frameworks
System Architecture:
+-------------+ +-------------------+ +-----------------+ +-------------+
| Client |------>| Node.js/Express |<----->| Infobip Service |------>| Infobip API |
| (Postman/UI)| | API Layer | | (Node.js SDK) | +-------------+
+-------------+ | (Routes, Ctrls) | +-----------------+
+-------------------+
|
V
+-------------------+
| Database (PG) |
| (Prisma ORM) |
+-------------------+Prerequisites:
- Free or paid Infobip account (sign up here)
- Node.js LTS version (18.x or later recommended) and npm
- PostgreSQL database (version 12 or later, local or cloud-based)
- Basic knowledge of Node.js, Express, REST APIs, and relational databases
- Registered phone number for your Infobip trial account (trial accounts have restrictions: typically 5–10 test numbers, limited sender IDs)
Time to complete: 60–90 minutes | Skill level: Intermediate
1. Setting Up Your Node.js Bulk SMS Project
Initialize your Node.js project, configure the directory structure, and install the required dependencies including the Infobip SDK, Express, and Prisma ORM.
-
Create Project Directory: Open your terminal and create a new project directory.
bashmkdir infobip-bulk-sms cd infobip-bulk-sms -
Initialize Node.js Project: Initialize npm with default settings using the
-yflag.bashnpm init -y -
Install Dependencies: Install Express, the Infobip SDK, dotenv, Prisma, and testing tools. These versions are compatible as of January 2025.
bashnpm install express @infobip-api/sdk dotenv pg @prisma/client npm install --save-dev prisma nodemon jest supertestexpress: Web framework@infobip-api/sdk: Official Infobip SDK for Node.jsdotenv: Loads environment variables from.envpg: PostgreSQL client for Node.js (used by Prisma)@prisma/client: Prisma's database clientprisma: Prisma CLI for migrations and studionodemon: Automatically restarts server during developmentjest: Testing frameworksupertest: HTTP assertion library for testing
-
Set up Project Structure: Create this directory structure:
plaintextinfobip-bulk-sms/ ├── prisma/ ├── src/ │ ├── controllers/ │ ├── routes/ │ ├── services/ │ ├── utils/ │ ├── middleware/ │ └── app.js # Main Express application setup ├── tests/ │ ├── integration/ │ └── unit/ ├── .env # Environment variables (DO NOT COMMIT) ├── .gitignore ├── package.json └── server.js # Entry point to start the server -
Configure
nodemon: Add development scripts topackage.json.json// package.json { // ... other fields "scripts": { "start": "node server.js", "dev": "nodemon server.js", "test": "jest" // Add prisma commands later } // ... rest of the file } -
Initialize Prisma: Set up Prisma with PostgreSQL as the provider.
bashnpx prisma init --datasource-provider postgresqlThis creates the
prisma/directory (if not already present) and aschema.prismafile, and updates your.envfile with aDATABASE_URLplaceholder. -
Configure Environment Variables: Create a
.envfile in the project root. Add this file to.gitignoreimmediately to prevent exposing credentials.plaintext# .env # Database DATABASE_URL="postgresql://<user>:<password>@<host>:<port>/<database>?schema=public" # Infobip Credentials INFOBIP_BASE_URL="YOUR_INFOBIP_BASE_URL" # Find this in your Infobip account API section INFOBIP_API_KEY="YOUR_INFOBIP_API_KEY" # Find this in your Infobip account API section # Application Settings PORT=3000 NODE_ENV=development # or production- Replace
<...>placeholders inDATABASE_URLwith your PostgreSQL credentials. - Find your
INFOBIP_BASE_URLandINFOBIP_API_KEYin the Infobip portal: Account Settings → API Keys (format:xxxxx.api.infobip.comfor base URL). - Production note: Use your hosting platform's environment variable system (AWS Secrets Manager, Heroku Config Vars, Kubernetes Secrets) instead of
.envfiles in production for enhanced security.
- Replace
-
Configure
.gitignore: Create a.gitignorefile in the root directory:text# .gitignore node_modules/ .env dist/ coverage/ *.log
2. Implementing Infobip SMS Service with Node.js SDK
Encapsulate all Infobip API interactions in a dedicated service module using the official Infobip Node.js SDK. This pattern separates business logic from API integration for better maintainability.
-
Create Infobip Service (
src/services/infobipService.js):javascript// src/services/infobipService.js const { Infobip, AuthType } = require('@infobip-api/sdk'); const logger = require('../utils/logger'); // We'll create this logger later // Load environment variables require('dotenv').config(); if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) { logger.error('Infobip Base URL or API Key not configured in .env file.'); throw new Error('Infobip credentials missing.'); } // Initialize Infobip client with API Key authentication const infobipClient = new Infobip({ baseUrl: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, authType: AuthType.ApiKey, }); /** * Sends the same SMS message to multiple recipients using Infobip's bulk send feature. * * @param {string[]} phoneNumbers - Array of recipient phone numbers in E.164 format (e.g., '+447123456789'). * @param {string} messageText - SMS message text (max 160 characters for standard GSM-7; 70 for Unicode/emojis; longer messages split into segments). * @param {string} [sender='InfoSMS'] - Sender ID (alphanumeric, max 11 characters; numeric IDs have different limits). Registration required in some countries (US, India). Check [Infobip sender ID rules](https://www.infobip.com/docs/sms/get-started). * @returns {Promise<object>} - The response object from the Infobip API, including the bulkId. * @throws {Error} - Throws an error if the API call fails. */ const sendBulkSms = async (phoneNumbers, messageText, sender = 'InfoSMS') => { if (!phoneNumbers || phoneNumbers.length === 0) { throw new Error('Recipient phone numbers array cannot be empty.'); } if (!messageText) { throw new Error('Message text cannot be empty.'); } // Map phone numbers to Infobip destination format const destinations = phoneNumbers.map(number => ({ to: number })); // Build message payload const payload = { messages: [ { from: sender, destinations: destinations, // Array of recipient objects text: messageText, }, // Add more message objects here for different texts/senders if needed ], // Optional: bulkId: 'YOUR_CUSTOM_BULK_ID' }; try { logger.info(`Attempting to send bulk SMS to ${phoneNumbers.length} recipients.`); logger.debug('Infobip Payload:', payload); // Log payload only in debug mode const infobipResponse = await infobipClient.channels.sms.send(payload); const { data } = infobipResponse; // Contains bulkId and message array with individual messageIds/statuses logger.info(`Infobip bulk SMS request successful. Bulk ID: ${data.bulkId}`); logger.debug('Infobip Response:', data); return { bulkId: data.bulkId, messages: data.messages.map(msg => ({ messageId: msg.messageId, to: msg.to, status: msg.status.name, statusGroup: msg.status.groupName, })), }; } catch (error) { logger.error('Infobip API Error:', error.response ? error.response.data : error.message); throw new Error(`Failed to send bulk SMS via Infobip: ${error.message}`); } }; module.exports = { sendBulkSms, };
How it works:
- Import and initialize: Load the Infobip SDK, validate credentials, and create the API client with API Key authentication.
sendBulkSmsfunction: Accepts phone numbers, message text, and optional sender ID. Maps phone numbers to destination objects, constructs the API payload, and sends the request.- Response handling: Returns structured data with
bulkId(tracks the entire batch) and individual message statuses. - Error handling: Catches API errors and throws descriptive exceptions.
3. Building the Express REST API for Bulk SMS
Create Express routes and controllers to expose the bulk SMS sending functionality via a RESTful API endpoint. This layer handles HTTP requests and orchestrates the SMS broadcast workflow.
-
Create Broadcast Controller (
src/controllers/broadcastController.js):javascript// src/controllers/broadcastController.js const infobipService = require('../services/infobipService'); const prisma = require('../utils/prismaClient'); // We'll create this later const logger = require('../utils/logger'); /** * Handle bulk SMS broadcast requests. * Expects { message: string, recipients: string[] } in request body. * Fetches recipients from database if `recipients` is omitted. */ const createBroadcast = async (req, res, next) => { const { message, recipients, sender } = req.body; // sender is optional // Input validation here is basic. Use express-validator middleware for production (see Section 7). // This controller orchestrates: request handling → database queries → Infobip service → database logging. if (!message) { return res.status(400).json({ error: 'Message content is required.' }); } let targetRecipients = recipients; try { // Fetch active recipients from database if not provided in request if (!targetRecipients || !Array.isArray(targetRecipients)) { logger.info('Recipient list not provided in request. Fetching active recipients from DB.'); const activeRecipients = await prisma.recipient.findMany({ where: { isActive: true }, select: { phone: true }, // Select only the phone number }); if (!activeRecipients || activeRecipients.length === 0) { return res.status(400).json({ error: 'No recipients provided and no active recipients found in the database.' }); } targetRecipients = activeRecipients.map(r => r.phone); logger.info(`Found ${targetRecipients.length} active recipients in DB.`); } if (targetRecipients.length === 0) { return res.status(400).json({ error: 'Recipient list cannot be empty.' }); } // Validate E.164 phone number format (basic regex; use google-libphonenumber for production) // Production example: const phoneUtil = require('google-libphonenumber').PhoneNumberUtil.getInstance(); // phoneUtil.isValidNumber(phoneUtil.parse(num, 'ZZ')) const invalidNumbers = targetRecipients.filter(num => !/^\+?[1-9]\d{1,14}$/.test(num)); if (invalidNumbers.length > 0) { logger.warn(`Request contains invalid phone numbers: ${invalidNumbers.join(', ')}`); return res.status(400).json({ error: 'Invalid phone number format detected. Use E.164 format (e.g., +15551234567).', invalidNumbers: invalidNumbers, }); } // Send bulk message via Infobip service const result = await infobipService.sendBulkSms(targetRecipients, message, sender); // Save broadcast record to database const broadcastRecord = await prisma.broadcast.create({ data: { message: message, status: 'SUBMITTED', // Initial status infobipBulkId: result.bulkId, recipientCount: targetRecipients.length, // Link recipients explicitly for detailed tracking in production } }); logger.info(`Broadcast record created with ID: ${broadcastRecord.id} and Bulk ID: ${result.bulkId}`); // Return 202 Accepted (async operation) res.status(202).json({ message: 'Broadcast request accepted successfully.', broadcastId: broadcastRecord.id, infobipBulkId: result.bulkId, submittedMessages: result.messages.length, // Individual message statuses available in result.messages }); } catch (error) { logger.error(`Failed to create broadcast: ${error.message}`); next(error); // Pass to centralized error handler } }; // TODO: Add getBroadcastStatus controller for status retrieval module.exports = { createBroadcast, }; -
Create Broadcast Routes (
src/routes/broadcastRoutes.js):javascript// src/routes/broadcastRoutes.js const express = require('express'); const broadcastController = require('../controllers/broadcastController'); // TODO: Import validation middleware (See Section 7) // const { validateBroadcastRequest } = require('../middleware/validators'); const router = express.Router(); // POST /api/v1/broadcasts - Create new broadcast router.post('/', /* validateBroadcastRequest, */ broadcastController.createBroadcast); // TODO: GET /api/v1/broadcasts/:id - Get broadcast status // router.get('/:id', broadcastController.getBroadcastStatus); module.exports = router; -
Set up Express App (
src/app.js):javascript// src/app.js const express = require('express'); const dotenv = require('dotenv'); const broadcastRoutes = require('./routes/broadcastRoutes'); const logger = require('./utils/logger'); const errorHandler = require('./middleware/errorHandler'); // TODO: Import security middleware (See Section 7) // const rateLimiter = require('./middleware/rateLimiter'); // const helmet = require('helmet'); dotenv.config(); const app = express(); // --- Middleware --- // app.use(helmet()); // TODO: Add security headers (See Section 7) app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Request logging app.use((req, res, next) => { logger.info(`${req.method} ${req.url}`); next(); }); // app.use(rateLimiter); // TODO: Add rate limiting (See Section 7) // --- Routes --- app.get('/health', (req, res) => res.status(200).json({ status: 'UP' })); app.use('/api/v1/broadcasts', broadcastRoutes); // --- Error Handling --- app.use((req, res, next) => { const error = new Error(`Not Found - ${req.originalUrl}`); res.status(404); next(error); }); // Centralized error handler (must be last) app.use(errorHandler); module.exports = app; -
Create Server Entry Point (
server.js):javascript// server.js const app = require('./src/app'); const logger = require('./src/utils/logger'); const PORT = process.env.PORT || 3000; const server = app.listen(PORT, () => { logger.info(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`); }); // Graceful Shutdown Handling process.on('SIGTERM', () => { logger.info('SIGTERM signal received: closing HTTP server'); server.close(() => { logger.info('HTTP server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT signal received: closing HTTP server'); server.close(() => { logger.info('HTTP server closed'); process.exit(0); }); }); // Handle unhandled promise rejections process.on('unhandledRejection', (err, promise) => { logger.error('Unhandled Rejection at:', promise, 'reason:', err); server.close(() => process.exit(1)); });
API Endpoint Example:
Test the POST /api/v1/broadcasts endpoint using cURL or Postman.
-
Method:
POST -
URL:
http://localhost:3000/api/v1/broadcasts -
Headers:
Content-Type: application/json -
Body (JSON):
json{ "message": "Hello from our bulk broadcast system! Special offer inside.", "recipients": ["+15551112222", "+447123456789", "+4915123456789"], "sender": "MyAppAlerts" }- Omit
recipientsto send to all active recipients in the database (once database is configured).
- Omit
-
cURL Example:
bashcurl -X POST http://localhost:3000/api/v1/broadcasts \ -H "Content-Type: application/json" \ -d '{ "message": "Hello from our bulk broadcast system!", "recipients": ["+15551112222", "+447123456789"], "sender": "MyAppAlerts" }' -
Example Success Response (202 Accepted):
json{ "message": "Broadcast request accepted successfully.", "broadcastId": "clxyzabc1234567890def", "infobipBulkId": "2034072219640523072", "submittedMessages": 3 } -
Example Error Response (400 Bad Request):
json{ "error": "Invalid phone number format detected. Use E.164 format (e.g., +15551234567).", "invalidNumbers": [ "12345" ] }
4. Infobip API Integration: Credentials and Configuration
You already initialized the Infobip client in infobipService.js using environment variables. Here's how to obtain these credentials.
-
Log in to Infobip: Access your Infobip account dashboard.
-
Navigate to API Keys: Look for "API Keys," "Developer Tools," or similar in the navigation menu.
-
Create/View API Key: Create a new API key or view an existing one. Ensure it has permissions for sending SMS messages.
-
Copy API Key: Securely copy the generated API Key. This is your
INFOBIP_API_KEY. -
Find Base URL: Your account-specific Base URL is displayed near the API Key or in the API settings/documentation section. It looks like
xxxxx.api.infobip.com. This is yourINFOBIP_BASE_URL. -
Update
.env: Paste the copied values into your.envfile.plaintext# .env # ... other variables INFOBIP_BASE_URL="YOUR_COPIED_BASE_URL" INFOBIP_API_KEY="YOUR_COPIED_API_KEY" # ... -
Restart Application: Restart your application to load the new environment variables.
nodemonusually handles this automatically.
Security Note: Never commit your .env file or hardcode API keys in source code. Use environment variables for all sensitive credentials. Rotate API keys regularly – generate new keys every 90 days and revoke old ones immediately.
API Rate Limits: Infobip enforces rate limits based on your account tier. Free tier typically allows 10–20 requests per second. Monitor your usage via the Infobip dashboard and upgrade your plan if needed.
5. Error Handling and Retry Logic for SMS Delivery
Robust error handling and logging are crucial for production systems.
-
Setup Logger (
src/utils/logger.js): This simple console-based logger is suitable for development. For production, usewinstonorpinofor advanced features like log levels, file transport, structured logging, and external log aggregation (Datadog, Splunk).javascript// src/utils/logger.js const logger = { info: (...args) => { console.log(`[INFO] ${new Date().toISOString()}:`, ...args); }, warn: (...args) => { console.warn(`[WARN] ${new Date().toISOString()}:`, ...args); }, error: (...args) => { console.error(`[ERROR] ${new Date().toISOString()}:`, ...args); }, debug: (...args) => { // Only log debug messages if NODE_ENV is 'development' if (process.env.NODE_ENV === 'development') { console.debug(`[DEBUG] ${new Date().toISOString()}:`, ...args); } }, }; module.exports = logger;Production Logger Example (winston):
javascript// Install: npm install winston const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ), }), ], }); module.exports = logger; -
Centralized Error Handler (
src/middleware/errorHandler.js): This middleware catches errors passed vianext(error)and sends a structured response.javascript// src/middleware/errorHandler.js const logger = require('../utils/logger'); const errorHandler = (err, req, res, next) => { // Log the full error stack trace for debugging logger.error(err.stack); // Determine status code const statusCode = err.statusCode || res.statusCode || 500; const finalStatusCode = statusCode < 400 ? 500 : statusCode; // Customize error response based on NODE_ENV const response = { message: err.message || 'An unexpected error occurred.', // Include stack trace only in development mode ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }), // Include custom error details if present ...(err.details && { details: err.details }), }; res.status(finalStatusCode).json(response); }; module.exports = errorHandler;- Make sure this middleware is added last in
src/app.js, after all routes.
- Make sure this middleware is added last in
-
Using the Error Handler: In controllers or services, wrap asynchronous operations in
try...catchblocks. If an error occurs, callnext(error)to pass it to the centralized handler.javascript// Example in src/controllers/broadcastController.js try { // ... async operations ... const result = await infobipService.sendBulkSms(/* ... */); // ... res.status(202).json(/* ... */); } catch (error) { logger.error(`Error in createBroadcast: ${error.message}`); next(error); // Pass to centralized handler } -
Retry Mechanisms: Network issues or temporary Infobip API glitches might occur. Implement retries to improve reliability. Use
async-retryfor exponential backoff and jitter.-
Install:
npm install async-retry -
Example Usage (in
infobipService.js):javascript// src/services/infobipService.js const retry = require('async-retry'); // ... other imports (Infobip, logger, etc.) const sendBulkSms = async (phoneNumbers, messageText, sender = 'InfoSMS') => { // ... input validation ... // Construct payload outside the retry loop const destinations = phoneNumbers.map(number => ({ to: number })); const payload = { messages: [{ from: sender, destinations, text: messageText }] }; // Wrap the API call with retry logic return await retry( async (bail, attemptNumber) => { logger.info(`Attempt ${attemptNumber} to send bulk SMS via Infobip...`); try { const infobipResponse = await infobipClient.channels.sms.send(payload); const { data } = infobipResponse; logger.info(`Infobip bulk SMS request successful on attempt ${attemptNumber}. Bulk ID: ${data.bulkId}`); logger.debug('Infobip Response:', data); return { bulkId: data.bulkId, messages: data.messages.map(msg => ({ messageId: msg.messageId, to: msg.to, status: msg.status.name, statusGroup: msg.status.groupName, })), }; } catch (error) { logger.warn(`Attempt ${attemptNumber} failed: ${error.message}`); if (error.response) { const status = error.response.status; // Bail on client errors (4xx) - retrying won't fix these if (status === 401 || status === 400 || status === 403 || status === 404 || status === 422) { logger.error(`Unrecoverable Infobip API error (${status}). Bailing out.`); bail(new Error(`Infobip API Error (${status}): ${error.message || 'Client Error'}`)); return; } // Retry on server errors (5xx) and network issues } throw error; } }, { retries: 3, // Total attempts = retries + 1 = 4 factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial delay: 1 second maxTimeout: 5000, // Maximum delay: 5 seconds randomize: true, // Add jitter to prevent thundering herd onRetry: (error, attempt) => { logger.warn(`Retrying Infobip call (attempt ${attempt}) due to error: ${error.message}`); }, } ); }; module.exports = { sendBulkSms }; -
Idempotency Note: Infobip's API is idempotent when you provide a custom
bulkId. If you retry with the samebulkId, Infobip won't send duplicate messages. To ensure idempotency, generate a uniquebulkId(e.g., using UUID) before the retry loop and include it in the payload.
Circuit Breaker Pattern: For production systems, implement a circuit breaker (using libraries like
opossum) to prevent cascading failures. A circuit breaker stops calling the Infobip API after repeated failures and retries after a cooldown period. -
6. Database Schema with Prisma and PostgreSQL
Use Prisma to define your database schema and interact with the database.
-
Define Schema (
prisma/schema.prisma): Update the schema file to include models forRecipientandBroadcast.prisma// prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Recipient { id String @id @default(cuid()) phone String @unique // Ensure phone numbers are unique name String? // Optional recipient name isActive Boolean @default(true) // Handle opt-outs or inactive users createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Broadcast { id String @id @default(cuid()) message String status String // e.g., PENDING, SUBMITTED, PROCESSING, COMPLETED, FAILED infobipBulkId String? @unique // Store the bulk ID from Infobip recipientCount Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Optional: Add relation to specific recipients for detailed tracking // recipients BroadcastRecipient[] } // Optional: Join table for many-to-many relationship if tracking per-recipient status // model BroadcastRecipient { // id String @id @default(cuid()) // broadcast Broadcast @relation(fields: [broadcastId], references: [id]) // broadcastId String // recipient Recipient @relation(fields: [recipientId], references: [id]) // recipientId String // messageId String? // Store individual message ID from Infobip // status String? // Store individual message status // submittedAt DateTime @default(now()) // updatedAt DateTime @updatedAt // // @@unique([broadcastId, recipientId]) // Ensure a recipient is only listed once per broadcast // } -
Run Migrations: Generate and apply the migration to create the database tables.
bashnpx prisma migrate dev --name initThis command:
- Creates a migration file in
prisma/migrations/ - Applies the migration to your database
- Generates the Prisma Client
- Creates a migration file in
-
Create Prisma Client Utility (
src/utils/prismaClient.js):javascript// src/utils/prismaClient.js const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['error'], }); module.exports = prisma; -
Seed Database (Optional): Create a seed script to populate test data.
javascript// prisma/seed.js const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); async function main() { await prisma.recipient.createMany({ data: [ { phone: '+15551112222', name: 'Alice', isActive: true }, { phone: '+447123456789', name: 'Bob', isActive: true }, { phone: '+4915123456789', name: 'Charlie', isActive: false }, ], }); console.log('Database seeded successfully'); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });Run the seed script:
bashnode prisma/seed.js
7. Security Best Practices for SMS Broadcasting APIs
Security Middleware
-
Install Security Packages:
bashnpm install helmet express-rate-limit express-validator -
Add Helmet for Security Headers (
src/app.js):javascriptconst helmet = require('helmet'); app.use(helmet()); -
Implement Rate Limiting (
src/middleware/rateLimiter.js):javascriptconst rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again later.', }); module.exports = limiter;Apply in
src/app.js:javascriptconst rateLimiter = require('./middleware/rateLimiter'); app.use('/api/', rateLimiter); -
Add Input Validation (
src/middleware/validators.js):javascriptconst { body, validationResult } = require('express-validator'); const validateBroadcastRequest = [ body('message') .trim() .notEmpty().withMessage('Message is required') .isLength({ max: 1600 }).withMessage('Message too long (max 1600 characters)'), body('recipients') .optional() .isArray().withMessage('Recipients must be an array') .custom((value) => { if (value && value.length > 1000) { throw new Error('Maximum 1000 recipients per request'); } return true; }), body('sender') .optional() .trim() .isLength({ max: 11 }).withMessage('Sender ID max 11 characters'), (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } next(); }, ]; module.exports = { validateBroadcastRequest };Apply in
src/routes/broadcastRoutes.js:javascriptconst { validateBroadcastRequest } = require('../middleware/validators'); router.post('/', validateBroadcastRequest, broadcastController.createBroadcast);
Additional Best Practices
- Environment-specific configs: Use different
.envfiles for development, staging, and production - HTTPS only: Enforce HTTPS in production
- CORS configuration: Configure CORS appropriately for your client applications
- Authentication: Add JWT or OAuth for production API authentication
- Monitoring: Integrate with services like Datadog, New Relic, or Prometheus
- Logging: Use structured logging and external log aggregation
8. Testing Your Bulk SMS Application
Create unit and integration tests for your application.
-
Configure Jest (
jest.config.js):javascriptmodule.exports = { testEnvironment: 'node', coverageDirectory: 'coverage', collectCoverageFrom: ['src/**/*.js'], testMatch: ['**/tests/**/*.test.js'], }; -
Unit Test Example (
tests/unit/infobipService.test.js):javascriptconst infobipService = require('../../src/services/infobipService'); describe('Infobip Service', () => { test('sendBulkSms throws error when phone numbers array is empty', async () => { await expect( infobipService.sendBulkSms([], 'Test message') ).rejects.toThrow('Recipient phone numbers array cannot be empty'); }); test('sendBulkSms throws error when message text is empty', async () => { await expect( infobipService.sendBulkSms(['+15551112222'], '') ).rejects.toThrow('Message text cannot be empty'); }); }); -
Integration Test Example (
tests/integration/broadcast.test.js):javascriptconst request = require('supertest'); const app = require('../../src/app'); describe('POST /api/v1/broadcasts', () => { test('returns 400 when message is missing', async () => { const response = await request(app) .post('/api/v1/broadcasts') .send({ recipients: ['+15551112222'] }); expect(response.status).toBe(400); expect(response.body).toHaveProperty('error'); }); test('returns 400 when recipients array is empty', async () => { const response = await request(app) .post('/api/v1/broadcasts') .send({ message: 'Test', recipients: [] }); expect(response.status).toBe(400); }); }); -
Run Tests:
bashnpm test
9. Deploying Your SMS Broadcast System
Deploy your application to a cloud platform.
Heroku Deployment
-
Install Heroku CLI and log in:
bashheroku login -
Create Heroku app:
bashheroku create your-app-name -
Add PostgreSQL addon:
bashheroku addons:create heroku-postgresql:mini -
Set environment variables:
bashheroku config:set INFOBIP_BASE_URL=your_base_url heroku config:set INFOBIP_API_KEY=your_api_key heroku config:set NODE_ENV=production -
Add Procfile:
web: node server.js release: npx prisma migrate deploy -
Deploy:
bashgit push heroku main
Docker Deployment
-
Create Dockerfile:
dockerfileFROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npx prisma generate EXPOSE 3000 CMD ["node", "server.js"] -
Create docker-compose.yml:
yamlversion: '3.8' services: app: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_URL=postgresql://user:password@db:5432/infobip - INFOBIP_BASE_URL=${INFOBIP_BASE_URL} - INFOBIP_API_KEY=${INFOBIP_API_KEY} depends_on: - db db: image: postgres:14-alpine environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=password - POSTGRES_DB=infobip volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data: -
Build and run:
bashdocker-compose up -d
Conclusion
You now have a production-ready bulk SMS broadcast system with:
- ✅ Express API with proper routing and controllers
- ✅ Infobip SDK integration for bulk SMS sending
- ✅ PostgreSQL database with Prisma ORM
- ✅ Error handling and retry mechanisms
- ✅ Security middleware (Helmet, rate limiting, validation)
- ✅ Structured logging
- ✅ Testing framework
- ✅ Deployment configurations
Next Steps
- Implement webhook handlers for Infobip delivery reports (track message delivery status)
- Add recipient management endpoints (create, update, delete recipients)
- Implement broadcast status retrieval endpoint
- Add job queuing for scheduled broadcasts (using Bull or Agenda)
- Set up monitoring and alerting (using Datadog, New Relic, or Prometheus)
- Implement advanced features (message templates, A/B testing, analytics)
Troubleshooting Common Issues
| Issue | Solution |
|---|---|
Infobip client is not initialized | Check .env file for correct INFOBIP_BASE_URL and INFOBIP_API_KEY values |
Invalid phone number format | Ensure phone numbers use E.164 format (e.g., +15551234567) |
Rate limit exceeded | Check your Infobip account tier and upgrade if needed, or implement request throttling |
Database connection error | Verify DATABASE_URL in .env and ensure PostgreSQL is running |
Prisma client not found | Run npx prisma generate to regenerate the client |
429 Too Many Requests | Reduce request frequency or upgrade Infobip plan |
401 Unauthorized | Verify API key is correct and has SMS permissions |
Cost Estimation
Infobip Pricing (approximate as of 2025):
- SMS costs vary by destination country: $0.01–$0.10 per message
- Free tier: 10–20 test messages
- Pay-as-you-go or monthly plans available
- Monitor usage via Infobip dashboard
Infrastructure Costs:
- Heroku: $7/month (Eco Dyno) + $5/month (Mini PostgreSQL) = $12/month
- AWS: ~$15–30/month (t3.micro EC2 + RDS PostgreSQL)
- DigitalOcean: ~$12/month (Basic Droplet + Managed PostgreSQL)
Additional Resources
Frequently Asked Questions
How to send bulk SMS messages with Node.js and Express?
Use the Infobip API with the Node.js SDK and Express to create a system that can handle sending a single SMS message to many recipients. This guide provides a comprehensive walkthrough, explaining each step in detail, from project setup and core functions to error handling and deployment techniques. You'll build an Express application that accepts requests to send SMS messages to a large list of recipients via Infobip's bulk sending feature.
How to test the bulk SMS API endpoint?
Use a tool like Postman to send POST requests to the `/api/v1/broadcasts` endpoint. Include a JSON body with the `message` and an array of `recipients`. Alternatively, you can omit `recipients` to send to all active recipients in the database.
What is Infobip and why use it for bulk SMS?
Infobip is a cloud communications platform offering APIs for various channels like SMS, voice, and chat apps. Its robust SMS API, accessible via the official Node.js SDK, is ideal for sending large volumes of SMS messages reliably and quickly. This guide utilizes Infobip for broadcasting alerts, information, or marketing messages.
How to set up a Node.js project for bulk SMS messaging?
Start by creating a new directory, initializing a Node.js project with `npm init -y`, and installing necessary dependencies like `express`, `@infobip-api/sdk`, `dotenv`, `pg`, `@prisma/client`, and development dependencies including `prisma`, `nodemon`, `jest`, and `supertest`. Structure your project with directories for controllers, routes, services, utils, middleware, and tests.
What database is used in this Infobip tutorial?
The guide utilizes PostgreSQL as its relational database and Prisma as an ORM for database interactions. However, the core concepts can be adapted to other ORMs supported by Prisma (like TypeORM, Sequelize) or databases (e.g., MySQL, MariaDB) with necessary code modifications. Remember to configure your database credentials in the .env file.
What is the purpose of the Infobip Node.js SDK?
The Infobip Node.js SDK simplifies interaction with the Infobip API. It handles authentication, request formatting, and response parsing, making it easier to integrate Infobip services into your Node.js application. The SDK is initialized with your Infobip Base URL and API key, which are stored as environment variables for security.
How to handle Infobip API credentials securely?
Store your `INFOBIP_BASE_URL` and `INFOBIP_API_KEY` in a `.env` file. This file should be added to your `.gitignore` to prevent it from being committed to version control. While suitable for development, for production environments, inject environment variables directly through your hosting platform or CI/CD pipeline for enhanced security.
What is the role of Prisma in the bulk SMS project?
Prisma acts as an Object-Relational Mapper (ORM), simplifying database interactions. It allows you to define your database schema using the Prisma Schema Language in `schema.prisma` and interact with the database using JavaScript. This guide uses Prisma with PostgreSQL but can be adapted for other databases and ORMs.
How to structure a Node.js Express application for bulk SMS?
Organize your project with a clear directory structure. This typically includes separate folders for controllers, routes, services, utils (like logging), and middleware. The `src/app.js` file handles the main Express application setup, while `server.js` serves as the entry point for starting the server.
Why use dotenv in a Node.js application?
Dotenv is a crucial package for managing environment variables, which hold sensitive information like API keys and database credentials. By loading these variables from a `.env` file, you keep them separate from your codebase, improving security. Remember to never commit the .env file to your repository.
How to implement error handling in Node.js for Infobip API calls?
Create a centralized error handler middleware that catches errors passed through `next(error)` and returns a structured error response. This simplifies error management and helps provide user-friendly error messages. Use try-catch blocks around async operations and log errors using a logger for debugging and tracking.
How to implement logging in Node.js for Infobip integration?
The guide provides a simple logger implementation using `console`. However, for production, libraries like `winston` or `pino` are recommended, offering more advanced logging features such as log levels, log rotation, and structured logging.
What is the system architecture for the bulk SMS application?
The system follows a client-server architecture: the client (e.g., Postman, UI) sends requests to the Node.js/Express API layer. The API layer interacts with both the Infobip service (via the Node.js SDK) and the PostgreSQL database (using Prisma). The Infobip service then communicates with the Infobip API to send the SMS messages.
How to send a bulk SMS message with a custom sender ID?
The `sendBulkSms` function in the `infobipService.js` accepts an optional `sender` parameter. Pass your desired alphanumeric sender ID to this parameter when calling the function. Be sure to consult Infobip's rules regarding sender IDs to ensure compliance.
What are the prerequisites for this bulk SMS guide?
You need a free or paid Infobip account, Node.js and npm installed, access to a PostgreSQL database, and basic knowledge of Node.js, Express, APIs, and databases. For testing with a free trial, ensure you have a registered phone number linked to your Infobip account.