Build Production-Ready Node.js Marketing SMS Campaigns with Express and Infobip
This guide provides a comprehensive walkthrough for building a Node.js and Express application capable of sending marketing SMS messages using the Infobip API. We'll cover everything from initial project setup and core SMS sending functionality to essential production considerations like error handling, security, compliance (specifically mentioning 10DLC for US traffic), and deployment.
While this guide discusses many elements crucial for a production environment (security, compliance, monitoring), the provided code examples serve as a starting point. A truly production-ready system would require significant additions, particularly around database integration, robust endpoint security, comprehensive logging, and potentially advanced queueing mechanisms.
By the end of this tutorial, you will have a functional Express API endpoint that can receive requests to send SMS messages via Infobip, incorporating best practices for security, reliability, and compliance awareness.
Project Overview and Goals
What We'll Build:
We will create a Node.js application using the Express framework. This application will expose an API endpoint designed to:
- Receive requests containing a recipient's phone number and a message body.
- Utilize the Infobip Node.js SDK to send the SMS message to the specified recipient via the Infobip API.
- Handle potential errors gracefully.
- Include basic security measures like rate limiting.
- Provide a foundation for storing recipient data (though full database implementation is outlined but kept simple for this guide).
- Emphasize the critical need for campaign registration (like 10DLC in the US) for sending marketing messages.
Problem Solved:
This guide addresses the need for developers to integrate reliable SMS sending capabilities into their Node.js applications for marketing purposes, ensuring they are aware of compliance requirements and follow best practices for integration.
Technologies Used:
- Node.js: The JavaScript runtime environment.
- Express: A minimal and flexible Node.js web application framework for building the API.
- Infobip API & Node.js SDK: For sending SMS messages. We'll use the official SDK (
infobip-api-client-ts
) for a cleaner integration than raw HTTP calls. - dotenv: To manage environment variables securely.
- express-rate-limit: To implement basic API rate limiting.
System Architecture:
+-------------+ +---------------------+ +-----------------+
| | HTTP | | API | |
| Client +-----> | Node.js/Express API +-------> | Infobip Platform|
| (e.g., Web, | Request | (Our Application) | Call | (SMS Gateway) |
| Mobile App)| | | | |
+-------------+ +---------------------+ +-----------------+
|
| (Optional Data Storage)
v
+-------------+
| Database |
| (e.g., JSON,|
| Postgres) |
+-------------+
Prerequisites:
- Node.js and npm (or yarn) installed.
- An Infobip Account (a free trial account is sufficient for testing, but be aware of limitations).
- Basic understanding of JavaScript, Node.js, Express, and REST APIs.
- A text editor or IDE (like VS Code).
- A tool for making API requests (like
curl
or Postman).
Expected Outcome:
A running Node.js Express application with a functional /send-sms
endpoint that uses the Infobip SDK to send messages. The guide will also detail the necessary steps and considerations discussed for making this production-ready, including compliance.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
Step 1: Create Project Directory
Open your terminal or command prompt and create a new directory for the project. Navigate into it.
mkdir infobip-node-campaigns
cd infobip-node-campaigns
Step 2: Initialize npm Project
Initialize a new Node.js project using npm. The -y
flag accepts the default settings.
npm init -y
This creates a package.json
file.
Step 3: Install Dependencies
We need Express for the server, the Infobip SDK for SMS sending, dotenv
for environment variables, and express-rate-limit
for basic security.
npm install express infobip-api-client-ts dotenv express-rate-limit
express
: Web framework.infobip-api-client-ts
: The official Infobip SDK for Node.js (works with JavaScript too). It simplifies API interactions compared to rawaxios
calls.dotenv
: Loads environment variables from a.env
file intoprocess.env
. Essential for keeping secrets out of code.express-rate-limit
: Basic middleware to limit repeated requests to public APIs.
Step 4: Project Structure
Create a basic project structure.
# In your project root (infobip-node-campaigns)
touch server.js .env .gitignore
mkdir config
touch config/infobipClient.js
server.js
: The main entry point for our Express application..env
: Stores sensitive information like API keys (will be created in the next step)..gitignore
: Specifies intentionally untracked files that Git should ignore (likenode_modules
and.env
).config/
: A directory for configuration files.config/infobipClient.js
: A module to initialize and configure the Infobip SDK client.
Step 5: Configure .gitignore
Add the following lines to your .gitignore
file to prevent committing sensitive data and unnecessary files:
# .gitignore
node_modules
.env
npm-debug.log
*.log
Step 6: Set Up Environment Variables (.env
)
Create a file named .env
in the project root. You'll need your Infobip API Key and Base URL.
- How to get Infobip Credentials:
- Log in to your Infobip account.
- Navigate to the homepage dashboard after login.
- Your API Key and Base URL should be visible on the main dashboard page, often in a section labeled ""API Keys"" or ""Developer Tools"". The Base URL will look something like
xxxxxx.api.infobip.com
. - Copy these values.
Add the following lines to your .env
file, replacing the placeholders with your actual credentials:
# .env
INFOBIP_API_KEY=your_infobip_api_key_here
INFOBIP_BASE_URL=your_infobip_base_url_here # e.g., yj3vxl.api.infobip.com
PORT=3000
INFOBIP_API_KEY
: Your secret API key for authentication.INFOBIP_BASE_URL
: The custom domain provided by Infobip for API requests.PORT
: The port number your Express server will run on.
Why .env
? Storing configuration, especially secrets like API keys, in environment variables is crucial for security. It prevents hardcoding sensitive data directly into your source code, making it easier to manage different environments (development, staging, production) and reducing the risk of accidental exposure.
2. Implementing Core Functionality (Sending SMS)
Now, let's configure the Infobip SDK client and write the logic to send an SMS.
Step 1: Configure the Infobip Client
Open config/infobipClient.js
and add the following code to initialize the SDK:
// config/infobipClient.js
const { Infobip, AuthType } = require('infobip-api-client-ts');
require('dotenv').config(); // Load environment variables
// Validate that required environment variables are set
if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) {
console.error(""Error: INFOBIP_BASE_URL and INFOBIP_API_KEY must be set in the .env file."");
process.exit(1); // Exit if configuration is missing
}
// Initialize Infobip client
const infobipClient = new Infobip({
baseUrl: process.env.INFOBIP_BASE_URL,
apiKey: process.env.INFOBIP_API_KEY,
authType: AuthType.ApiKey,
});
module.exports = infobipClient;
- We import the necessary components from the SDK and
dotenv
. - We explicitly check if the required environment variables are loaded. This prevents runtime errors if the
.env
file is missing or misconfigured. - We create an instance of the
Infobip
client, passing ourbaseUrl
,apiKey
, and specifyingAuthType.ApiKey
. - We export the configured client instance for use elsewhere in our application.
Step 2: Create the Express Server and SMS Sending Route
Open server.js
and set up the Express application and the route for sending SMS.
// server.js
const express = require('express');
const infobipClient = require('./config/infobipClient');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 3000;
// Middleware to parse JSON bodies
app.use(express.json());
// --- SMS Sending Route ---
app.post('/send-sms', async (req, res) => {
const { to, text } = req.body;
// Basic Input Validation
if (!to || !text) {
return res.status(400).json({ error: 'Missing required fields: ""to"" and ""text"".' });
}
// Basic phone number format check (E.164).
// WARNING: This regex is very basic. For production, use a robust library like
// 'google-libphonenumber' for proper E.164 validation and parsing.
if (!/^\+[1-9]\d{1,14}$/.test(to)) {
return res.status(400).json({ error: 'Invalid phone number format. Use E.164 format (e.g., +14155552671).' });
}
console.log(`Received request to send SMS to: ${to}, message: ""${text}""`);
try {
// Use the Infobip SDK to send an SMS
const sendSmsResponse = await infobipClient.channels.sms.send({
messages: [
{
destinations: [{ to: to }],
text: text,
// from: ""InfoSMS"" // Optional: Sender ID (alpha or numeric). Subject to registration/regulations.
},
],
});
console.log('Infobip API Response:', JSON.stringify(sendSmsResponse, null, 2));
// Check the response status from Infobip
// Note: The success check below (PENDING/DELIVERED) is common but might not cover
// all non-error states. Consult Infobip API documentation for the specific endpoint
// to confirm robust status handling for all possible scenarios.
const messageStatus = sendSmsResponse.data?.messages?.[0]?.status;
if (messageStatus && (messageStatus.groupName === 'PENDING' || messageStatus.groupName === 'DELIVERED')) {
res.status(200).json({
message: 'SMS successfully sent or queued.',
details: sendSmsResponse.data.messages[0],
});
} else {
// If status is not explicitly successful, treat as potential issue
console.warn(`Infobip reported a non-terminal/non-pending status: ${messageStatus?.groupName || 'Unknown'}`);
res.status(500).json({
error: 'Infobip reported an issue sending the SMS or returned an unexpected status.',
details: messageStatus || 'No status information received.',
fullResponse: sendSmsResponse.data
});
}
} catch (error) {
console.error('Error sending SMS via Infobip:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
// Provide a more specific error message if available from Infobip API error response
const errorMessage = error.response?.data?.requestError?.serviceException?.text || 'Failed to send SMS due to an internal server error or Infobip API issue.';
const statusCode = error.response?.status || 500; // Use Infobip's status code if available
res.status(statusCode).json({ error: errorMessage, details: error.response?.data });
}
});
// --- Health Check Route ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// --- Start the Server ---
// Note: For the integration tests in Section 13 to run as written,
// you would need to export the 'app' instance: module.exports = app;
// However, this might interfere with starting the server directly via 'node server.js'.
// Consider using a separate file to create and export 'app' if needed for testing.
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
console.log(`SMS sending endpoint available at POST http://localhost:${port}/send-sms`);
});
- We import
express
and our configuredinfobipClient
. - We use
express.json()
middleware to parse incoming JSON request bodies. - The
POST /send-sms
route handles the logic:- It extracts the
to
(recipient phone number) andtext
(message content) from the request body. - Input Validation: Performs basic checks for required fields and a simple E.164 format check. Strongly emphasizes that robust validation (e.g., using
google-libphonenumber
) is essential for production. - It calls
infobipClient.channels.sms.send()
with the message details. - Response Handling: Logs the response and checks for
PENDING
orDELIVERED
status groups. Includes a warning that other states might exist and recommends checking Infobip docs. - Error Handling: A
try...catch
block handles errors, logs them, and sends an appropriate error response, extracting details from the Infobip response if possible.
- It extracts the
- A simple
/health
endpoint is included. - A note is added regarding exporting
app
for testing purposes. - The server starts listening.
Why the SDK? Using the official Infobip SDK (infobip-api-client-ts
) abstracts away the complexities of making raw HTTP requests (constructing URLs, setting headers, handling authentication tokens, parsing responses). It provides typed methods and often includes built-in retry logic or better error handling structures, leading to cleaner, more maintainable, and less error-prone code.
3. Building a Complete API Layer
Our core functionality relies on the POST /send-sms
endpoint. Let's detail its usage and provide testing examples.
API Endpoint Documentation:
-
Endpoint:
POST /send-sms
-
Description: Sends a single SMS message to a specified recipient via Infobip.
-
Authentication: None implemented directly on our API layer in this basic example (though the server authenticates with Infobip using the API Key). In production, you would secure this endpoint (e.g., with API keys, JWT, OAuth).
-
Request Body: JSON
to
(string, required): The recipient's phone number in E.164 format (e.g.,+14155552671
).text
(string, required): The content of the SMS message.
-
Success Response (200 OK): JSON
{ ""message"": ""SMS successfully sent or queued."", ""details"": { ""to"": ""+14155552671"", ""status"": { ""groupId"": 1, ""groupName"": ""PENDING"", ""id"": 26, ""name"": ""PENDING_ACCEPTED"", ""description"": ""Message accepted and queued for processing."" }, ""messageId"": ""unique-message-id-from-infobip"" } }
-
Error Responses:
400 Bad Request
: Missing fields or invalid phone number format.
{ ""error"": ""Missing required fields: \""to\"" and \""text\""."" }
{ ""error"": ""Invalid phone number format. Use E.164 format (e.g., +14155552671)."" }
401 Unauthorized
(if Infobip rejects the API Key):
{ ""error"": ""Invalid login details"", ""details"": { ""requestError"": { ""serviceException"": { ""messageId"": ""UNAUTHORIZED"", ""text"": ""Invalid login details"" } } } }
500 Internal Server Error
(or other status codes from Infobip): General failure during processing or sending.
{ ""error"": ""Infobip reported an issue sending the SMS."", ""details"": { /* ... detailed error object from Infobip ... */ } }
Testing with curl
:
-
Start your server:
node server.js
-
Send a request: Open another terminal window and run the following
curl
command. Replace<your_phone_number_e164>
with your actual phone number in E.164 format (the one registered with your Infobip free trial account if applicable).curl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""<your_phone_number_e164>"", ""text"": ""Hello from Node.js and Infobip!"" }'
You should receive a 200 OK
response in the terminal and an SMS message on your phone shortly after.
Testing with Postman:
- Create a new request.
- Set the method to
POST
. - Enter the URL:
http://localhost:3000/send-sms
. - Go to the ""Body"" tab, select ""raw"", and choose ""JSON"" from the dropdown.
- Paste the JSON payload into the body:
{ ""to"": ""<your_phone_number_e164>"", ""text"": ""Hello from Postman, Node.js, and Infobip!"" }
- Click ""Send"".
4. Integrating with Necessary Third-Party Services (Infobip)
We've already integrated Infobip using the SDK. This section reiterates the configuration details.
Configuration Steps:
- Obtain Credentials: As detailed in Section 1, Step 6, get your API Key and Base URL from your Infobip dashboard.
- Secure Storage: Store these credentials in the
.env
file in your project root. Never commit.env
files to version control.# .env INFOBIP_API_KEY=your_infobip_api_key_here INFOBIP_BASE_URL=your_infobip_base_url_here PORT=3000
- Load Variables: Use the
dotenv
package early in your application (require('dotenv').config();
) to load these variables intoprocess.env
. - Initialize SDK: Use the loaded environment variables to configure the Infobip SDK client instance, as shown in
config/infobipClient.js
.
Environment Variable Details:
INFOBIP_API_KEY
:- Purpose: Authenticates your application's requests to the Infobip API.
- Format: A long alphanumeric string.
- How to Obtain: From the main page of your Infobip portal dashboard.
INFOBIP_BASE_URL
:- Purpose: Specifies the unique endpoint domain for your Infobip account's API requests.
- Format: A domain name like
xxxxxx.api.infobip.com
. - How to Obtain: From the main page of your Infobip portal dashboard, usually listed alongside the API Key.
PORT
:- Purpose: Defines the network port on which your Express server will listen for incoming HTTP requests.
- Format: A number (typically between 1024 and 65535).
3000
is a common default for development. - How to Obtain: You choose this value based on your preferences and environment constraints.
Fallback Mechanisms:
For critical SMS notifications, consider:
- Retry Logic: Implement retries with exponential backoff directly in your code if the SDK doesn't handle it sufficiently, especially for transient network errors or temporary Infobip issues (e.g., 5xx errors). Libraries like
async-retry
can help. - Monitoring & Alerting: Set up alerts (Section 10) for high failure rates from the Infobip API.
- Secondary Provider (Advanced): In highly critical scenarios, you might integrate a second SMS provider as a fallback, though this adds significant complexity.
5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
We've implemented basic error handling, but let's refine logging.
Error Handling Strategy:
- Catch Specific Errors: Catch errors from the Infobip SDK call within the route handler (
try...catch
). - Extract Details: Attempt to parse specific error messages and status codes from the Infobip API response within the
catch
block (error.response.data
). - Consistent Responses: Return standardized JSON error responses to the client (e.g.,
{ ""error"": ""message"", ""details"": { ... } }
). - Appropriate Status Codes: Use HTTP status codes that reflect the error type (400 for bad input, 401 for auth issues from Infobip, 500 for server/upstream errors).
Logging:
Our current console.log
and console.error
are basic. For production, use a dedicated logging library like winston
or pino
for structured logging, different log levels, and configurable outputs (file, console, external services).
Example with Basic Logging Refinement:
Modify the catch
block in server.js
:
// Inside the catch block of app.post('/send-sms', ...) in server.js
} catch (error) {
// Log the error with more context
const timestamp = new Date().toISOString();
const logPayload = {
timestamp: timestamp,
level: 'ERROR',
message: 'Error sending SMS via Infobip',
route: '/send-sms',
// WARNING: Logging 'input.to' (phone number) might violate PII policies.
// Consider masking or omitting sensitive data like phone numbers in production logs.
input: { to: to /* Mask this? */, text: text },
errorMessage: error.message,
infobipResponse: error.response ? error.response.data : null,
stack: error.stack // Optional: include stack trace for detailed debugging
};
console.error(JSON.stringify(logPayload, null, 2)); // Use JSON for structured logging
const errorMessage = error.response?.data?.requestError?.serviceException?.text || 'Failed to send SMS due to an internal server error or Infobip API issue.';
const statusCode = error.response?.status || 500;
res.status(statusCode).json({
error: errorMessage,
details: error.response?.data // Send back Infobip's error structure if available
});
}
- Structured Logging: Logging errors as JSON objects makes them easier to parse and analyze by log management systems (like Datadog, Splunk, ELK stack).
- Context: Including timestamp, route, input (carefully!), and the original Infobip response provides valuable debugging information.
- PII Warning: Explicitly warns about logging the recipient's phone number (
to
) and suggests considering masking or omission in production environments due to potential PII (Personally Identifiable Information) concerns.
Retry Mechanisms:
While the Infobip SDK might handle some retries, explicitly adding them for specific error types (like network timeouts or 5xx errors) can improve resilience.
- Strategy: Use exponential backoff (wait longer between each retry) to avoid overwhelming the API. Limit the number of retries.
- Implementation: Libraries like
async-retry
simplify this. You'd wrap theinfobipClient.channels.sms.send(...)
call within the retry logic. This is generally more applicable to backend processes than direct API responses where timeouts might be shorter.
6. Creating a Database Schema and Data Layer (Conceptual)
For a real marketing campaign system, you need to store recipient information, opt-in status, and potentially campaign details.
Conceptual Schema (using pseudocode/simplified Prisma example):
// Example schema using Prisma syntax (schema.prisma)
generator client {
provider = ""prisma-client-js""
}
datasource db {
provider = ""postgresql"" // Or ""mysql"", ""sqlite"", ""mongodb""
url = env(""DATABASE_URL"")
}
model Recipient {
id String @id @default(cuid())
phoneNumber String @unique // E.164 format
firstName String?
lastName String?
subscribed Boolean @default(true) // Tracks opt-in status
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Add relationships to Campaigns if needed
// campaigns Campaign[]
}
model Campaign {
id String @id @default(cuid())
name String
description String?
// Status could track registration status (Draft, Submitted, Registered, Rejected)
status String @default(""DRAFT"")
infobipCampaignId String? // Store the ID returned by Infobip upon creation/registration
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// recipients Recipient[] // Many-to-many relationship if needed
}
Data Layer Implementation:
- ORM/Driver: Use an Object-Relational Mapper (ORM) like Prisma or Sequelize (for SQL databases) or Mongoose (for MongoDB) to interact with your database.
- Repository Pattern: Abstract database logic into repositories (e.g.,
recipientRepository.js
) to keep route handlers clean. - Migrations: Use the ORM's migration tools (e.g.,
prisma migrate dev
,sequelize db:migrate
) to manage schema changes systematically.
Integration:
Instead of sending to a single number from the request body, your API endpoint might:
- Accept a
campaignId
orlistId
. - Fetch subscribed recipients associated with that campaign/list from the database.
- Iterate through the recipients and send SMS messages (potentially in batches using Infobip's multi-destination capability).
- Update send status or logs in the database.
Note: Implementing a full database layer is beyond the scope of this initial guide but is a critical next step for managing marketing lists and campaigns effectively.
7. Adding Security Features
Protecting your API and user data is paramount.
Input Validation and Sanitization:
- Validation: We implemented basic validation for
to
andtext
. Use libraries likejoi
orexpress-validator
for more complex validation rules (length, format, allowed characters). For phone numbers,google-libphonenumber
is highly recommended for robust parsing and validation. - Sanitization: While less critical for SMS text itself compared to HTML output, be mindful if incorporating user input directly into messages. Avoid constructing messages in ways that could be abused if the input source isn't trusted.
Rate Limiting:
Prevent abuse and brute-force attacks by limiting the number of requests a client can make.
Implementation with express-rate-limit
:
Add this middleware near the top of server.js
, after express.json()
:
// server.js (near the top, after app.use(express.json()))
const 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 after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply the rate limiting middleware to all requests
// Or apply selectively: app.use('/send-sms', limiter);
app.use(limiter);
// ... rest of your routes (app.post('/send-sms', ...), etc.)
- This configuration limits each IP address to 100 requests per 15 minutes. Adjust
windowMs
andmax
based on your expected traffic and security requirements.
API Key Security:
- Never Expose Client-Side: Your Infobip API key must only be used server-side.
- Environment Variables: Use
.env
files and environment variables in deployment. - Rotation: Consider rotating API keys periodically.
Securing Your Endpoint:
The /send-sms
endpoint itself is currently open. In production, you would need to secure it:
- API Keys: Require clients calling your API to provide a separate API key in headers.
- JWT/OAuth: Implement standard authentication/authorization flows if users are triggering the SMS sends.
8. Handling Special Cases Relevant to the Domain
Phone Number Formatting (E.164):
- Requirement: Infobip (and most international carriers) requires phone numbers in E.164 format (e.g.,
+14155552671
,+442071838750
). This includes the+
sign and the country code. - Validation: Implement robust validation to ensure numbers adhere to this format before sending to the API. As mentioned, libraries like
google-libphonenumber
(via its Node.js port) are the standard for parsing, validating, and formatting phone numbers globally and are strongly recommended over basic regex for production use.
SMS Character Limits and Encoding:
- Standard GSM-7: 160 characters per segment.
- Unicode (UCS-2): 70 characters per segment (used for non-Latin characters, emojis).
- Concatenation: Longer messages are split into multiple segments, which might incur higher costs. Be aware of this limit when crafting messages. Infobip handles the concatenation, but character count matters for pricing and user experience.
Opt-Out Handling (Compliance Requirement):
- Legal Necessity: Regulations like TCPA (US), GDPR (EU), and CASL (Canada) require clear opt-out mechanisms for marketing messages.
- Implementation:
- Include opt-out instructions in your messages (e.g., ""Reply STOP to unsubscribe"").
- Configure Infobip Webhooks (or use their platform settings) to listen for incoming messages (MO - Mobile Originated).
- Process incoming
STOP
,UNSUBSCRIBE
, etc., keywords to update the recipient'ssubscribed
status in your database immediately. - Ensure your sending logic checks the
subscribed
status before sending any marketing message.
Sender ID:
- Alphanumeric Senders: You can often set a custom sender ID (like your brand name, e.g.,
InfoSMS
in the code comment). - Restrictions: Sender ID registration and capabilities vary significantly by country and carrier. Some countries require pre-registration, others only allow numeric senders, and some override alphanumeric senders. Consult Infobip documentation and local regulations. Using a dedicated sending number (like a 10DLC number in the US) is often more reliable.
9. Implementing Performance Optimizations
Batch Sending:
-
Capability: The Infobip
sms/2/text/advanced
endpoint (which the SDK uses) supports sending the same message text to multiple destinations in a single API call, or different messages to different destinations. -
Benefit: Reduces HTTP overhead compared to making one API call per recipient.
-
Implementation: Modify the
messages
array structure passed to the SDK:// Example: Sending the same message to multiple recipients const destinations = recipients.map(r => ({ to: r.phoneNumber })); // Assuming recipients is an array of { phoneNumber: '...' } const sendSmsResponse = await infobipClient.channels.sms.send({ messages: [ { destinations: destinations, text: ""Your bulk marketing message here!"", }, ], }); // Example: Sending different messages (less common for bulk marketing) // const messages = recipients.map(r => ({ // destinations: [{ to: r.phoneNumber }], // text: `Hi ${r.firstName}, your personalized message!` // })); // const sendSmsResponse = await infobipClient.channels.sms.send({ messages });
Check the specific SDK documentation for the most efficient way to structure batch requests.
Asynchronous Processing:
- For very large campaigns, avoid blocking the API response while sending thousands of SMS.
- Strategy:
- The API endpoint receives the request and validates it.
- It queues a job (e.g., using Redis, RabbitMQ, or a cloud queuing service like AWS SQS).
- A separate background worker process picks up jobs from the queue.
- The worker fetches recipients and sends SMS messages (possibly in batches) via Infobip.
- The initial API response returns quickly (e.g.,
202 Accepted
) indicating the job is queued.
Database Query Optimization:
- Ensure efficient database queries when fetching recipient lists (use indexes on
phoneNumber
andsubscribed
status). - Fetch only the necessary data (
phoneNumber
).
10. Adding Monitoring, Observability, and Analytics
Health Checks:
- We added a basic
/health
endpoint. Expand this to check database connectivity or other critical dependencies if applicable. Monitoring services frequently poll this endpoint.
Performance Metrics:
- API Latency: Track the response time of your
/send-sms
endpoint. - Infobip API Latency: Log the time taken for the
infobipClient.channels.sms.send()
call. - Throughput: Monitor the number of requests per minute/second your API handles.
- Error Rates: Track the percentage of failed requests to your API and failed SMS sends via Infobip.
Logging and Tracing:
- Centralized Logging: Send structured logs (like the JSON example in Section 5) to a log aggregation service (ELK, Datadog, Splunk, Grafana Loki).
- Distributed Tracing: Implement tracing (e.g., using OpenTelemetry) to follow requests across your service and the Infobip API call, helping diagnose bottlenecks.
Delivery Reports (DLRs):
- Purpose: Infobip can send webhook notifications about the final delivery status of each SMS (Delivered, Failed, Undelivered, Expired).
- Implementation:
- Create a new endpoint in your Express app to receive these webhook POST requests from Infobip.
- Configure the DLR webhook URL in your Infobip account settings.
- Process incoming DLRs to update message status in your database or trigger alerts for high failure rates.
Analytics:
- Track key campaign metrics: messages sent, delivery rates, opt-out rates, potentially conversion rates if applicable. Use this data to optimize campaigns.
11. Writing Unit and Integration Tests
Testing ensures your application works correctly and prevents regressions.
Unit Tests:
- Focus: Test individual functions or modules in isolation.
- Tools: Use frameworks like
jest
ormocha
with assertion libraries likechai
. Use spies/stubs/mocks (e.g.,sinon
orjest.fn()
) to isolate dependencies like the Infobip SDK or database calls. - Examples:
- Test input validation logic (e.g., valid/invalid phone numbers).
- Test the Infobip client configuration logic.
- Test helper functions for formatting messages or data.
Integration Tests:
-
Focus: Test the interaction between different parts of your application, including the API endpoint and potentially mocked external services.
-
Tools: Use
supertest
along withjest
ormocha
to make HTTP requests to your running (or in-memory) Express app. Mock the Infobip SDK to avoid sending real SMS during tests. -
Example (
supertest
withjest
):// Example: __tests__/server.test.js const request = require('supertest'); // Assuming server.js exports the app for testing: const app = require('../server'); // Or manage app lifecycle within tests (start/stop) // Mock the Infobip client BEFORE importing the app that uses it const mockSend = jest.fn(); jest.mock('../config/infobipClient', () => ({ channels: { sms: { send: mockSend, }, }, })); // Now require the app AFTER the mock is set up const app = require('../server'); // Adjust path as needed describe('POST /send-sms', () => { beforeEach(() => { // Reset mocks before each test mockSend.mockClear(); }); it('should return 400 if ""to"" is missing', async () => { const res = await request(app) .post('/send-sms') .send({ text: 'Test message' }); expect(res.statusCode).toEqual(400); expect(res.body.error).toContain('Missing required fields'); }); it('should return 400 if phone number format is invalid', async () => { const res = await request(app) .post('/send-sms') .send({ to: '12345', text: 'Test message' }); expect(res.statusCode).toEqual(400); expect(res.body.error).toContain('Invalid phone number format'); }); it('should call Infobip SDK and return 200 on valid request', async () => { // Mock a successful Infobip response mockSend.mockResolvedValue({ data: { messages: [{ to: '+14155552671', status: { groupName: 'PENDING' }, messageId: 'mock-message-id' }] } }); const res = await request(app) .post('/send-sms') .send({ to: '+14155552671', text: 'Valid test message' }); expect(res.statusCode).toEqual(200); expect(res.body.message).toContain('SMS successfully sent or queued'); expect(res.body.details.messageId).toBe('mock-message-id'); // Check if the mock function was called correctly expect(mockSend).toHaveBeenCalledWith({ messages: [{ destinations: [{ to: '+14155552671' }], text: 'Valid test message' }] }); }); it('should return 500 if Infobip SDK throws an error', async () => { // Mock an Infobip API error response const apiError = new Error('Infobip API Error'); apiError.response = { status: 401, data: { requestError: { serviceException: { text: 'Invalid credentials' } } } }; mockSend.mockRejectedValue(apiError); const res = await request(app) .post('/send-sms') .send({ to: '+14155552671', text: 'Test message' }); expect(res.statusCode).toEqual(401); // Should reflect Infobip's status code expect(res.body.error).toBe('Invalid credentials'); }); }); describe('GET /health', () => { it('should return 200 OK', async () => { const res = await request(app).get('/health'); expect(res.statusCode).toEqual(200); expect(res.body.status).toBe('UP'); }); });
- Mocking: Shows how to use
jest.mock
to replace the actualinfobipClient
with a mock, allowing you to control its behavior (mockResolvedValue
,mockRejectedValue
) and assert that it was called correctly (toHaveBeenCalledWith
). - Testing Routes: Uses
supertest
to send requests to/send-sms
and/health
and checks status codes and response bodies.
- Mocking: Shows how to use
12. Addressing Compliance and Regulatory Requirements
Sending marketing SMS, especially in bulk, is subject to strict regulations. Failure to comply can result in significant fines and carrier blocking.
Key Compliance Areas:
- Consent (Opt-In):
- You must have explicit, documented consent from recipients before sending them marketing messages. The type of consent required (e.g., express written consent for TCPA in the US) varies by region.
- Clearly state what users are opting into (frequency, type of content).
- Keep records of consent (timestamp, source, IP address if applicable).
- Opt-Out Mechanism:
- Provide a clear, easy way for recipients to unsubscribe (e.g., ""Reply STOP to opt out"").
- Honor opt-out requests immediately and automatically. Process keywords like STOP, UNSUBSCRIBE, CANCEL, END, QUIT.
- Maintain a suppression list of opted-out numbers and ensure they are never messaged again for marketing purposes.
- Sender Identification:
- Clearly identify who is sending the message (your brand/company name).
- Content Restrictions:
- Avoid prohibited content categories (SHAFT: Sex, Hate, Alcohol, Firearms, Tobacco - specific rules vary).
- Be truthful and not misleading.
- Quiet Hours:
- Respect time zones and avoid sending messages during legally restricted hours (e.g., early morning or late night according to the recipient's local time). TCPA in the US restricts calls/texts to 8 AM - 9 PM recipient's time.
- 10DLC (US A2P Messaging):
- Requirement: For Application-to-Person (A2P) messaging to US numbers using standard 10-digit long codes, businesses must register their Brand and Campaigns with The Campaign Registry (TCR).
- Process: This is typically done through your SMS provider (Infobip). You provide details about your company (Brand) and how you plan to use SMS (Campaign Use Case, e.g., Marketing, Notifications).
- Benefits: Registered traffic generally has higher throughput and better deliverability compared to unregistered traffic, which is heavily filtered or blocked by US carriers.
- Action: Work with Infobip support or use their platform tools to complete 10DLC registration before launching campaigns to US recipients.
- Other Regions: Be aware of specific regulations in other target countries (e.g., GDPR in Europe, CASL in Canada).
Disclaimer: This is not legal advice. Consult with legal counsel familiar with telecommunications law in your target regions to ensure full compliance.
Integration:
- Your database schema (Section 6) should include a
subscribed
field and potentially a timestamp for opt-in/opt-out. - Your sending logic must check the
subscribed
status before sending. - Implement webhook handling (Section 8) to process
STOP
replies automatically.
13. Deployment Considerations
Moving from development to production requires careful planning.
Environment Configuration:
- Use environment variables (
.env
locally, system environment variables or secrets management in production) for all configuration, especially secrets (API keys, database URLs). Do not hardcode secrets. - Maintain separate configurations for development, staging, and production environments.
Hosting Options:
- Platform as a Service (PaaS): Heroku, Render, Fly.io, Google App Engine, AWS Elastic Beanstalk. Simplifies deployment and scaling.
- Containers: Dockerize your application and deploy using orchestrators like Kubernetes (EKS, GKE, AKS) or simpler container services (AWS Fargate, Google Cloud Run). Provides consistency across environments.
- Virtual Private Server (VPS): Linode, DigitalOcean, AWS EC2. More control but requires manual server management (OS updates, security patching, process management).
Process Management:
- Use a process manager like
pm2
ornodemon
(for development) to keep your Node.js application running, handle crashes, and manage logs.# Install pm2 globally npm install pm2 -g # Start your app pm2 start server.js --name infobip-sms-api # Monitor pm2 monit # List processes pm2 list
Database:
- Use a managed database service (AWS RDS, Google Cloud SQL, MongoDB Atlas) for reliability, backups, and scaling.
Load Balancing:
- If expecting high traffic, deploy multiple instances of your application behind a load balancer (provided by your hosting platform or set up manually with Nginx/HAProxy).
CI/CD Pipeline:
- Set up Continuous Integration/Continuous Deployment (CI/CD) using tools like GitHub Actions, GitLab CI, Jenkins, or CircleCI.
- Automate testing, building (if needed), and deployment to staging/production environments upon code commits.
Security Hardening:
- Keep Node.js and all dependencies updated (
npm audit
). - Configure firewalls to restrict access.
- Use HTTPS for all communication (usually handled by PaaS or load balancers).
- Implement robust authentication/authorization for your API endpoint (Section 7).
Monitoring and Alerting (Production Grade):
- Integrate with production-grade monitoring tools (Datadog, New Relic, Dynatrace, Prometheus/Grafana) for deeper insights into performance, errors, and resource usage.
- Set up alerts for critical issues (high error rates, high latency, service down, high DLR failure rate).
Conclusion
This guide walked through building a foundational Node.js and Express application for sending marketing SMS messages using the Infobip API and SDK. We covered project setup, core sending logic, API design, essential integrations, error handling, security basics, compliance awareness (including 10DLC), performance considerations, testing strategies, and deployment factors.
Remember, the provided code is a starting point. Building a truly robust, scalable, and compliant production system requires further development, particularly in database integration, advanced error handling, comprehensive logging/monitoring, queueing for large volumes, rigorous testing, and strict adherence to legal requirements.
By following these steps and considerations, you are well-equipped to integrate reliable and compliant SMS marketing capabilities into your Node.js applications. Always prioritize security, compliance, and user experience when working with communication APIs.