Developer Guide: Node.js Express Bulk SMS Messaging with Infobip
This guide provides a comprehensive walkthrough for building a production-ready Node.js application using the Express framework to send bulk/broadcast SMS messages via the Infobip API and its official Node.js SDK. We'll cover everything from project setup and core implementation to error handling, security, and deployment.
Project Overview and Goals
What We're Building:
We will create a Node.js Express application featuring an API endpoint capable of receiving a list of phone numbers and a message text. This application will then leverage the Infobip API via its Node.js SDK to efficiently send that message as an SMS to all specified recipients.
Problem Solved:
This application addresses the need to programmatically send the same SMS message to multiple recipients simultaneously (broadcast/bulk messaging). This is common for marketing campaigns, notifications, alerts, or announcements where reaching a large audience quickly via SMS is required. Manually sending these messages or using basic single-send API calls is inefficient and error-prone at scale.
Technologies Used:
- Node.js: A JavaScript runtime environment enabling server-side development. Chosen for its asynchronous nature, large ecosystem (npm), and suitability for I/O-bound tasks like API interactions.
- Express.js: A minimal and flexible Node.js web application framework. Provides robust features for web and mobile applications, simplifying routing, middleware, and request/response handling.
- Infobip API and Node.js SDK (
@infobip-api/sdk
): Infobip provides communication APIs, including SMS. We'll use their official Node.js SDK to simplify interactions, handle authentication, and structure API requests correctly. Using the SDK is preferred over manual HTTP requests for maintainability and leveraging built-in features. - dotenv: A zero-dependency module that loads environment variables from a
.env
file intoprocess.env
. Essential for managing sensitive information like API keys securely. - (Optional but Recommended) SQLite3: A simple file-based relational database. We'll use it to demonstrate storing recipient lists persistently, though you can adapt the principles to other databases like PostgreSQL or MySQL.
- (Recommended) Pino: A fast, low-overhead JSON logger.
- (Recommended) Express Validator: Middleware for request data validation.
- (Recommended) Helmet: Middleware for setting security-related HTTP headers.
- (Recommended) Express Rate Limit: Middleware for basic rate limiting.
System Architecture:
+-----------------+ +---------------------+ +-----------------+ +-----------------+
| Client (e.g., |----->| Node.js/Express App |----->| Infobip Node.js |----->| Infobip API |
| Postman, Frontend)| | (API Endpoint) | | SDK | | (SMS Gateway) |
+-----------------+ +----------+----------+ +-----------------+ +--------+--------+
| ^
| (Optional) |
v |
+----------+----------+ | (SMS Sent)
| Database (SQLite) |<-----+
| (Recipient List) |
+---------------------+
- A client (like Postman or a frontend application) sends a POST request to our Express API endpoint (
/api/broadcast
). - The Express app receives the request_ validates the payload (recipients_ message).
- (Optional) The app retrieves recipient phone numbers from a database.
- The app uses the Infobip Node.js SDK to format the bulk SMS request.
- The SDK sends the request (including authentication) to the Infobip API.
- Infobip processes the request and sends the SMS messages to the recipients' handsets.
- Infobip returns a response (including
bulkId
and individual message statuses) to the SDK_ which relays it back to the Express app. - The Express app sends a success or error response back to the client.
Prerequisites:
- Infobip Account: You need an active Infobip account. Sign up at infobip.com. Note any limitations of free trial accounts (e.g._ sending only to your registered phone number).
- Node.js and npm (or yarn): A recent LTS version of Node.js installed. Verify with
node -v
andnpm -v
. (Download Node.js). - Basic JavaScript/Node.js Knowledge: Familiarity with JavaScript syntax_ asynchronous programming (
async/await
)_ and basic Node.js concepts. - Basic Command Line/Terminal Usage: Ability to navigate directories and run commands.
- (Optional) API Testing Tool: Like Postman or
curl
for testing the API endpoint.
Expected Outcome:
By the end of this guide_ you will have a functional Node.js Express application with a single API endpoint (POST /api/broadcast
) that accepts a list of phone numbers and a message_ sends the SMS to all recipients via Infobip_ and returns the results from the Infobip API. You will also understand how to manage configuration_ handle basic errors_ and implement essential security practices.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for the project_ then navigate into it.
mkdir nodejs-infobip-bulk-sms cd nodejs-infobip-bulk-sms
-
Initialize Node.js Project: This creates a
package.json
file to manage project details and dependencies.npm init -y
(The
-y
flag accepts the default settings) -
Install Dependencies: We need Express for the web server_ the Infobip SDK_ dotenv for environment variables_ and optionally sqlite3 for the database.
npm install express @infobip-api/sdk dotenv sqlite3 pino pino-pretty express-validator helmet express-rate-limit
express
: The web framework.@infobip-api/sdk
: The official Infobip SDK for Node.js.dotenv
: Loads environment variables from.env
.sqlite3
: Driver for SQLite database (remove if not using the database part).pino
_pino-pretty
: Logger and development formatter.express-validator
: Input validation middleware.helmet
: Security headers middleware.express-rate-limit
: Rate limiting middleware.
-
Install Development Dependencies (Optional but Recommended):
nodemon
automatically restarts the server during development when file changes are detected.npm install --save-dev nodemon
-
Configure
package.json
Scripts: Openpackage.json
and modify thescripts
section to add convenient commands for starting the server.// package.json { // ... other properties ""scripts"": { ""start"": ""node index.js""_ ""dev"": ""nodemon index.js""_ // Use nodemon for development ""test"": ""echo \""Error: no test specified\"" && exit 1"" // We'll add tests later }_ // ... other properties }
-
Create Project Structure: Let's organize our code logically. Create the following files and folders:
nodejs-infobip-bulk-sms/ ├── .env # Environment variables (API Keys_ etc.) - DO NOT COMMIT ├── .gitignore # Specifies intentionally untracked files that Git should ignore ├── .dockerignore # Specifies files to ignore during Docker build ├── Dockerfile # For containerizing the application ├── index.js # Main application entry point ├── package.json ├── package-lock.json ├── node_modules/ # Created by npm install (usually ignored by git) └── src/ # Source code directory ├── config/ # Configuration files │ ├── infobip.js │ └── logger.js ├── services/ # Business logic (sending SMS_ database interaction) │ ├── infobipService.js │ └── recipientService.js # (Optional DB service) ├── routes/ # API route definitions │ └── api.js ├── controllers/ # Request handlers for routes │ └── messagingController.js ├── middleware/ # Custom middleware (e.g._ validation handlers) │ └── validation.js └── database/ # Database related files (optional) ├── database.js └── schema.sql
-
Create
.gitignore
: Prevent sensitive files and unnecessary folders from being committed to version control. Create a.gitignore
file in the project root:# .gitignore node_modules/ .env npm-debug.log* yarn-debug.log* yarn-error.log* *.sqlite # Ignore database files if using SQLite
-
Create
.env
File: This file will hold our sensitive configuration. Create.env
in the project root:# .env - DO NOT COMMIT THIS FILE # Infobip Credentials INFOBIP_API_KEY=<YOUR_INFOBIP_API_KEY> INFOBIP_BASE_URL=<YOUR_INFOBIP_BASE_URL> # e.g., xyz123.api.infobip.com # Application Settings PORT=3000 NODE_ENV=development # Set to 'production' in deployment LOG_LEVEL=info # Database (Optional - for SQLite) DATABASE_PATH=./mydatabase.sqlite
- IMPORTANT: Replace placeholders
<YOUR_INFOBIP_API_KEY>
and<YOUR_INFOBIP_BASE_URL>
with your actual Infobip credentials (see next section). Never commit.env
files to Git.
- IMPORTANT: Replace placeholders
-
Basic
index.js
Setup: Set up the main Express application file.// index.js require('dotenv').config(); // Load .env variables early const express = require('express'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const logger = require('./src/config/logger'); const apiRoutes = require('./src/routes/api'); // Optional: Database setup // const { initializeDatabase } = require('./src/database/database'); const app = express(); const PORT = process.env.PORT || 3000; // --- Middleware --- // Security Headers app.use(helmet()); // Rate Limiting (Apply before routes) const apiLimiter = 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 after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers handler: (req, res, /*next, options*/) => { logger.warn(`Rate limit exceeded for IP: ${req.ip}`); res.status(429).json({ error: 'Too many requests, please try again later.' }); } }); app.use('/api', apiLimiter); // Apply limiter specifically to /api routes // Body Parser app.use(express.json()); // Parse JSON request bodies // Request Logger (Example using Pino) app.use((req, res, next) => { logger.info({ method: req.method, url: req.url, ip: req.ip }, 'Incoming request'); next(); }); // --- Database Initialization (Optional) --- // initializeDatabase() // .then(() => logger.info('Database initialized successfully.')) // .catch(err => logger.error({ err }, 'Database initialization failed:')); // --- Routes --- app.get('/', (req, res) => { res.send('Infobip Bulk SMS Service is running!'); }); app.use('/api', apiRoutes); // Mount API routes under /api // --- Centralized Error Handler --- // MUST be defined AFTER all other app.use() and routes app.use((err, req, res, next) => { logger.error({ err: err, stack: err.stack }, 'Unhandled Error'); const statusCode = err.statusCode || 500; let message = 'Something went wrong!'; // Generic default // Avoid leaking sensitive details in production for 500 errors if (statusCode !== 500 || process.env.NODE_ENV !== 'production') { message = err.message || message; } else { message = 'An internal server error occurred.'; } res.status(statusCode).json({ error: message }); }); // --- Start Server --- app.listen(PORT, () => { logger.info(`Server running on port ${PORT} in ${process.env.NODE_ENV} mode`); // Check if Infobip credentials are loaded (basic check) if (!process.env.INFOBIP_API_KEY || !process.env.INFOBIP_BASE_URL) { logger.warn('WARNING: Infobip API Key or Base URL not found in environment variables. SMS sending will fail.'); } });
Now we have a basic Express project structure ready for implementing the core functionality.
2. Obtaining Infobip API Credentials
To interact with the Infobip API, you need an API Key and your account-specific Base URL.
-
Log in to Infobip: Access your Infobip Portal account (https://portal.infobip.com/).
-
Navigate to API Keys: Go to the homepage dashboard or navigate through the menu (the exact location might change slightly, but look for sections like "Developers," "API," or "Settings"). Find the "API Keys management" section.
-
Create or View API Key: You might have a default key, or you can create a new one. Copy the generated API Key. This is a secret value – treat it like a password.
-
Find Your Base URL: On the same API Keys page or in the main developer/API section, find your unique Base URL. It typically looks like
[unique_identifier].api.infobip.com
. Copy this URL. -
Update
.env
: Paste the copied API Key and Base URL into your.env
file created in the previous step:# .env INFOBIP_API_KEY=YourCopiedApiKeyString........ INFOBIP_BASE_URL=yourUniqueId.api.infobip.com PORT=3000 NODE_ENV=development LOG_LEVEL=info DATABASE_PATH=./mydatabase.sqlite
- Security: Ensure the
.env
file is listed in your.gitignore
and is never committed to your repository. Use environment variables in production deployment environments instead of committing the.env
file.
- Security: Ensure the
3. Implementing Core Functionality (Bulk SMS Sending)
Now, let's implement the service responsible for interacting with the Infobip SDK and the logger.
-
Configure Logger: Set up the Pino logger.
// src/config/logger.js const pino = require('pino'); const logger = pino({ level: process.env.LOG_LEVEL || 'info', // Control log level via env var transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', // Pretty print logs in development options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname', } } : undefined, // Use default JSON output in production }); module.exports = logger;
-
Configure Infobip Client: Create the configuration file to initialize the SDK client instance.
// src/config/infobip.js const { Infobip, AuthType } = require('@infobip-api/sdk'); const logger = require('./logger'); // Use our logger if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) { logger.error(""FATAL ERROR: Infobip Base URL or API Key is missing in environment variables.""); // In a real app, you might want to exit or throw a more specific startup error // process.exit(1); throw new Error(""Missing Infobip credentials in environment.""); } let infobipClient; try { infobipClient = new Infobip({ baseUrl: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, authType: AuthType.ApiKey, // Specify API Key authentication }); logger.info('Infobip client configured successfully.'); } catch (error) { logger.error({ err: error }, ""Failed to initialize Infobip client.""); throw error; // Re-throw to prevent app start with bad config } module.exports = infobipClient;
- Why
AuthType.ApiKey
? Infobip supports different authentication methods. We explicitly tell the SDK to use the API Key provided. - Why check environment variables here? It ensures that if the client is imported, the necessary configuration is expected to be present, failing early if not.
- Why
-
Create Infobip Service: This service will contain the logic to format the request and call the Infobip SDK method for sending SMS.
// src/services/infobipService.js const infobipClient = require('../config/infobip'); const logger = require('../config/logger'); /** * Sends a bulk SMS message using the Infobip API. * @param {string[]} phoneNumbers - An array of recipient phone numbers in international format (e.g., '447123456789'). * @param {string} messageText - The text content of the SMS message. * @param {string} [sender='InfoSMS'] - The sender ID (alphanumeric). Check Infobip regulations for your country. * @returns {Promise<object>} - A promise that resolves with the Infobip API response (bulkId, messages array). * @throws {Error} - Throws an error if the API call fails or input is invalid. */ async function sendBulkSms(phoneNumbers, messageText, sender = 'InfoSMS') { if (!phoneNumbers || !Array.isArray(phoneNumbers) || phoneNumbers.length === 0) { logger.warn('Attempted to send SMS with empty or invalid recipient list.'); throw new Error('Recipient phone numbers array cannot be empty.'); } if (!messageText || typeof messageText !== 'string' || messageText.trim() === '') { logger.warn('Attempted to send SMS with empty message text.'); throw new Error('Message text cannot be empty.'); } // Validate phone number format (basic check - consider using libphonenumber-js for production robustness) const invalidNumbers = []; const validDestinations = phoneNumbers .map(num => String(num).replace(/\D/g, '')) // Clean non-digits .filter(cleanedNum => { // Basic check for 10-15 digits (adjust regex if needed for specific country codes/lengths) const isValid = /^\d{10,15}$/.test(cleanedNum); if (!isValid) { invalidNumbers.push(String(num)); // Log original invalid number } return isValid; }) .map(validNum => ({ to: validNum })); // Map to Infobip format if (invalidNumbers.length > 0) { logger.warn({ invalidNumbers }, `Found ${invalidNumbers.length} potentially invalid phone number formats.`); // Decide policy: throw error, proceed with valid ones, etc. // For now, we proceed with valid ones, but log a warning. if (validDestinations.length === 0) { throw new Error('No valid phone number destinations after validation.'); } } // Map valid phone numbers to the required Infobip destination format const destinations = validDestinations; // Already mapped above try { logger.info(`Attempting to send SMS via Infobip to ${destinations.length} valid recipients. Sender: ${sender}`); const response = await infobipClient.channels.sms.send({ type: 'text', messages: [ { from: sender, destinations: destinations, text: messageText.trim(), // Ensure trimmed message // See Infobip docs for more options: https://www.infobip.com/docs/api/channels/sms/sms-messaging/outbound-sms/send-sms-message }, ], }); logger.info({ bulkId: response.data?.bulkId, messageCount: response.data?.messages?.length }, 'Infobip API Response Received'); // Log detailed response only if needed or at debug level // logger.debug({ responseData: response.data }, 'Full Infobip API Response Data'); // The SDK response structure nests the actual data return response.data; // Contains { bulkId, messages: [...] } } catch (error) { // Log detailed error information const errorDetails = error.response ? error.response.data : error.message; logger.error({ error: errorDetails, status: error.response?.status }, 'Error sending SMS via Infobip'); // Create a more user-friendly error to throw upwards const errorMessage = error.response?.data?.requestError?.serviceException?.text || `Infobip API Error (Status: ${error.response?.status || 'N/A'})`; const serviceError = new Error(errorMessage); // Attach status code if available, useful for the central error handler if (error.response?.status) { serviceError.statusCode = error.response.status; } throw serviceError; } } module.exports = { sendBulkSms, };
- Why
infobipClient.channels.sms.send
? This is the specific SDK method for sending SMS messages. - Why the
messages
array? The API is designed to handle multiple message definitions in one call. For a simple broadcast, we use one message object containing multipledestinations
. - Why
destinations
array? Each object represents one recipient (to
field). - Why
response.data
? The SDK wraps the raw HTTP response. The actual data returned by Infobip (bulkId
,messages
status array) is within the.data
property. - Improved Validation: Basic validation now cleans numbers and logs warnings for potentially invalid ones, proceeding only with valid formats. It throws an error if no valid numbers remain.
- Error Handling: We wrap the call in
try...catch
, log detailed errors using Pino, and throw a new, potentially cleaner error message upwards, attaching the status code if possible.
- Why
4. Building the API Layer (Express Route)
Let's create the Express route and controller that will use our infobipService
.
-
Create Validation Middleware Helper: This simplifies handling validation results in routes.
// src/middleware/validation.js const { validationResult } = require('express-validator'); const logger = require('../config/logger'); const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { logger.warn({ errors: errors.array({ onlyFirstError: true }), url: req.originalUrl }, 'Validation errors occurred'); // Return only the first error per field for cleaner responses return res.status(400).json({ errors: errors.array({ onlyFirstError: true }) }); } next(); // Proceed to controller if validation passes }; module.exports = { handleValidationErrors };
-
Create Messaging Controller: This file handles the logic for incoming requests to messaging endpoints.
// src/controllers/messagingController.js const { sendBulkSms } = require('../services/infobipService'); const logger = require('../config/logger'); // Optional: To get recipients from DB // const { getAllSubscribedRecipients } = require('../services/recipientService'); /** * Handles the POST request to /api/broadcast * Expects a JSON body validated by middleware: * { "recipients": ["+1...", "+44..."], "message": "Your message", "sender": "OptionalSenderID" } * OR { "message": "Your message" } if fetching recipients from DB is enabled. */ async function handleBroadcast(req, res, next) { // Input is assumed valid here due to validation middleware const { recipients, message, sender } = req.body; // sender is optional try { let targetRecipients = recipients; // --- Logic to determine recipients --- if (!targetRecipients) { // If recipients array is not provided in the body, try fetching from DB (if enabled) // --- Uncomment and adapt this block if using the database feature --- /* logger.info('No recipients in request body, attempting to fetch from database...'); try { targetRecipients = await getAllSubscribedRecipients(); // Assumes this returns array of phone numbers if (!targetRecipients || targetRecipients.length === 0) { logger.warn('No recipients found in the database.'); // Use a custom error object or status code if desired const err = new Error('Not Found: No recipients found in the database.'); err.statusCode = 404; throw err; } logger.info(`Fetched ${targetRecipients.length} recipients from database.`); } catch (dbError) { // Pass DB errors to the central handler return next(dbError); } */ // --- End Optional DB Block --- // If DB fetch is not enabled or failed, and recipients were not in body, it's an error. // This situation should ideally be caught by validation if 'recipients' is mandatory without DB. // If 'recipients' is optional AND DB fetch isn't active/successful, throw error. if (!targetRecipients) { // Check again after potential DB attempt const err = new Error('Bad Request: Recipient list is required.'); err.statusCode = 400; throw err; } } // --- End recipient determination --- // --- Call the Infobip Service --- logger.info(`Initiating broadcast to ${targetRecipients.length} recipients.`); const result = await sendBulkSms(targetRecipients, message, sender); // Pass optional sender // --- Send Success Response --- res.status(200).json({ message: `SMS broadcast initiated successfully to ${result.messages?.length || 0} valid recipients.`, // Use count from response infobipResponse: result, // Contains { bulkId, messages: [...] } }); } catch (error) { // Log the error here if needed (already logged in service/handler) // Pass the error to the centralized error handler middleware next(error); } } module.exports = { handleBroadcast, };
- Relies on Validation Middleware: Assumes
express-validator
has already checked the basic structure and types ofrecipients
andmessage
. - Database Logic Structure: The database fetching logic is clearly separated and commented out, making it easier to enable/disable. The default path assumes recipients come from the request.
- Error Handling: All errors (validation, service, DB) are consistently passed to the central error handler using
next(error)
. - Success Message: Uses the count of messages returned by Infobip for a more accurate success message.
- Relies on Validation Middleware: Assumes
-
Create API Routes with Validation: Define the endpoints, link them to controllers, and apply validation rules.
// src/routes/api.js const express = require('express'); const { body } = require('express-validator'); const { handleBroadcast } = require('../controllers/messagingController'); const { handleValidationErrors } = require('../middleware/validation'); const router = express.Router(); // Validation rules for the broadcast route const validateBroadcast = [ body('message') .trim() .notEmpty().withMessage('Message cannot be empty.') .isString().withMessage('Message must be a string.') .isLength({ max: 1600 }).withMessage('Message length exceeds typical limits.'), // Add reasonable length limit // Make 'recipients' optional *only if* DB fetching logic is active in the controller. // If DB is not used, 'recipients' should be required. // Example: Assuming DB fetch is optional/commented out, recipients are required: body('recipients') .notEmpty().withMessage('Recipients array is required.') // Remove .notEmpty() if DB fetch is primary .isArray({ min: 1 }).withMessage('Recipients must be an array with at least one number.') .custom((recipients) => { if (!Array.isArray(recipients)) return false; // Ensure it's an array first // Basic format check (can be stricter or use libphonenumber-js) // Checks if every item is a string and looks like a plausible international number (cleaned) if (!recipients.every(num => typeof num === 'string' && /^\+?\d{10,15}$/.test(String(num).trim().replace(/[\s()-]/g,'')))) { throw new Error('One or more recipient phone numbers have an invalid format (expected international numbers like +15551234567).'); } // Optional: Check for duplicates const uniqueRecipients = new Set(recipients.map(num => String(num).trim().replace(/[\s()-]/g,''))); if (uniqueRecipients.size !== recipients.length) { throw new Error('Duplicate phone numbers found in the recipient list.'); } return true; }), body('sender') .optional() .trim() .isLength({ min: 1, max: 11 }).withMessage('Sender ID must be between 1 and 11 characters.') // Infobip sender rules vary; this is a common alphanumeric example. Adjust as needed. .matches(/^[a-zA-Z0-9]+$/).withMessage('Sender ID should typically be alphanumeric (check Infobip rules).') ]; // POST /api/broadcast router.post( '/broadcast', validateBroadcast, // Apply validation rules handleValidationErrors, // Handle any validation errors handleBroadcast // Proceed to controller if valid ); // Add other API routes here if needed module.exports = router;
- Improved Validation: Added max length for message, stricter phone number format check (allows optional
+
, removes common separators before checking digits), and optional duplicate check.
- Improved Validation: Added max length for message, stricter phone number format check (allows optional
-
Mount Routes in
index.js
: Ensure the main application file uses these routes (already done in the setup step).// index.js (relevant part) const apiRoutes = require('./src/routes/api'); // ... other requires/middleware app.use('/api', apiRoutes); // Mount API routes under /api prefix // ... error handling and listen
-
Test the Endpoint: Start the server:
npm run dev
Use
curl
or Postman to send a POST request:Using
curl
:curl -X POST http://localhost:3000/api/broadcast \ -H "Content-Type: application/json" \ -d '{ "recipients": ["+15551234567", "+15559876543"], "message": "Hello from the Node.js Bulk Sender! (Test 1)" }'
(Replace the example
+1555...
numbers with your actual, internationally formatted test phone numbers, e.g.,+447123456789
. Remember potential trial account restrictions on recipient numbers.)Expected JSON Response (Success):
{ "message": "SMS broadcast initiated successfully to 2 valid recipients.", "infobipResponse": { "bulkId": "some-unique-bulk-id-generated-by-infobip", "messages": [ { "to": "15551234567", "status": { /* ... Infobip status object ... */ }, "messageId": "some-unique-message-id-1" }, { "to": "15559876543", "status": { /* ... Infobip status object ... */ }, "messageId": "some-unique-message-id-2" } ] } }
Expected JSON Response (Error - e.g., bad input):
{ "errors": [ { "type": "field", "msg": "Message cannot be empty.", "path": "message", "location": "body" } ] }
Expected JSON Response (Error - e.g., Infobip API failure): (Handled by the central error handler)
{ "error": "Infobip API Error: Invalid login details" // Or other specific error from Infobip/service layer }
You now have a working API endpoint capable of sending bulk SMS via Infobip!
5. (Optional) Creating a Database Schema and Data Layer (SQLite Example)
Storing recipients in a database allows for managing larger lists and potentially tracking send statuses or user preferences.
(Ensure sqlite3
is installed: npm install sqlite3
)
-
Define Database Schema: Create a file to define the table structure.
-- src/database/schema.sql -- Table to store recipients CREATE TABLE IF NOT EXISTS recipients ( id INTEGER PRIMARY KEY AUTOINCREMENT, phone_number TEXT NOT NULL UNIQUE, -- Ensure uniqueness, store in consistent international format first_name TEXT, last_name TEXT, subscribed BOOLEAN DEFAULT 1, -- 1 for true, 0 for false created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Optional: Index for faster lookups by phone number CREATE INDEX IF NOT EXISTS idx_phone_number ON recipients (phone_number); -- Optional: Index for faster lookups of subscribed users CREATE INDEX IF NOT EXISTS idx_subscribed ON recipients (subscribed); -- Optional: Trigger to update 'updated_at' timestamp automatically CREATE TRIGGER IF NOT EXISTS update_recipients_updated_at AFTER UPDATE ON recipients FOR EACH ROW BEGIN UPDATE recipients SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id; END;